|
|
|
/** |
|
* Usage: |
|
* const { nblUID } = CreateNibolsUID({ uniqueChars: "ABCDEFGHIJKLMNPQRSUVWXYZ235679", size: 8, prime: 999867530999 }); |
|
* const uid = nblUID(1); |
|
*/ |
|
const CreateNibolsUID = (() => { |
|
|
|
const DEFAULT_CONFIG = Object.freeze({ |
|
/** Define each unique symbol that the end UID may have: */ |
|
alphabet: 'ABCDEFGHIJKLMNPQRSUVWXYZ235679', |
|
// this is 30 chars and omits similar characters like 0 and O. |
|
/** Ok we have baseNum different symbols. */ |
|
// baseNum: BigInt(alphabet.length), |
|
|
|
/** The UID will always have size many chars: */ |
|
size: 8n, |
|
/** modulus is (how many unique symbols are available) to the power of (the desired length of the UID) */ |
|
// modulus: BigInt(baseNum ** size), |
|
|
|
/** Now after calculating modulus, we must choose a prime number, who is close to modulus, that is COPRIME to modulus. */ |
|
// prime: 999867530999n, // To which i chose 999 "8675-389" 999 lol |
|
/** Finally we need the modular inverse of prime with respect to modulus: */ |
|
// invPrime: modInverse(cfg.prime, cfg.modulus) |
|
}); |
|
|
|
function NibolsUIDFactory(config) { |
|
const preConfig = { ...DEFAULT_CONFIG, ...config }; |
|
const baseNum = BigInt(preConfig.alphabet.length); |
|
const modulus = BigInt(baseNum ** preConfig.size); |
|
const invPrime = modInverse(preConfig.prime, modulus); |
|
const cfg = { ...preConfig, baseNum, modulus, invPrime }; |
|
|
|
|
|
/** |
|
* Get a constant length unique identifier for a positive integer number. |
|
* @param {Number} n positive integer number |
|
* @returns {String} unique identifier |
|
*/ |
|
function nblEncodeUID(n) { |
|
try { |
|
if (n == null) { throw new Error(`NibolsUID.encode: please do not try to encode '${typeof n}'.`); } |
|
if (!Number.isFinite(n)) { throw new Error(`NibolsUID.encode: This only works with numbers, you cannot encode '${typeof n}' stuff like ${n} yet.`); } |
|
if (!Number.isInteger(n)) { throw new Error(`NibolsUID.encode: Unable to encode decimal ${n} because of the ambiguity of not being able to choose between encoding either ${Math.ceil(n)} or ${Math.floor(n)} or any number in between, please provide an integer.`); } |
|
if (n < 0) { throw new Error(`NibolsUID.encode: Failed to encode negative number ${n} because the concept of a Unique Negative Identifier has not yet been implemented.`); } |
|
} catch (e) { |
|
throw new Error(`NibolsUID.encode: Failed to make the message explaining why the input is invalid. Please only try to encode positive integers.`); |
|
} |
|
|
|
// console.log('cfg :>> ', cfg); |
|
let x = (BigInt(n) * cfg.prime) % cfg.modulus; |
|
let chars = []; |
|
for (let i = 0; i < cfg.size; i++) { |
|
chars.push(cfg.alphabet[Number(x % cfg.baseNum)]); |
|
x = x / cfg.baseNum; |
|
} |
|
return chars.reverse().join(''); |
|
} |
|
|
|
/** |
|
* Get the number used to generate this UID. |
|
* |
|
* ⚠️ IMPORTANT: Decode is config-dependant ! |
|
* > |
|
* > To ensure it will decode correctly the UIDs, |
|
* > ensure the decoder has the same configuration |
|
* > as the encoder. |
|
* > |
|
* > Make sure to always decode the string UIDs by |
|
* > an NibolsUID instance created with the same |
|
* > configuration: alphabet, size, and prime must |
|
* > be the same. |
|
*[ |
|
"AFRDCV7U", |
|
42, |
|
276662824446, |
|
202949699793, |
|
"AAAAABX2", |
|
159700981134, |
|
42, |
|
192137064411, |
|
"3B2LKDR6", |
|
1245225811379, |
|
1079924085291, |
|
42 |
|
] |
|
* For instance: |
|
* ```javascript |
|
* |
|
* // Using default (length=9, prime=999867530999) |
|
* const NbDefault = CreateNibolsUID(); |
|
* const uidDefo = NbDefault.nblUID(4815162342); // "AFRDCV7U" |
|
* |
|
* // Different prime (37 instead of 999867530999): |
|
* const NbPrime37 = CreateNibolsUID({ prime: 37 }); |
|
* const uidPrime37 = NbPrime37.nblUID(4815162342); // "AAAAABX2" |
|
* |
|
* // Different length (4 instead of 9): |
|
* const NbLen4 = CreateNibolsUID({ alphabet: "SK8TE4LIFZABCDGHJMNPQRUVWXY235679" }); |
|
* const uidLen4 = NbLen4.nblUID(4815162342); // "3B2LKDR6" |
|
* |
|
* // Now decoding back: ╭─────────────────────╥─────────────────────╥─────────────────────╮ |
|
* // ┯ │ uidDefo ║ uidPrime37 ║ uidLen4 │ |
|
* ╭─────────────────────╆━━━━━━━━━━━━━━━━━━━━━╫━━━━━━━━━━━━━━━━━━━━━╫━━━━━━━━━━━━━━━━━━━━━┥ |
|
* │ NibolsUID (String) ┃ "AFRDCV7U" ║ "AAAAABX2" ║ "3B2LKDR6 │ |
|
* ├─────────────────────╂───────────────┬─────╫───────────────┬─────╫───────────────┬─────┤ |
|
* │ NbDefault ┃ 42 │ Yes ║ 276662824446 │ ║ 202949699793 │ │ |
|
* ├─────────────────────╂───────────────┼─────╫───────────────┼─────╫───────────────┼─────┤ |
|
* │ NbPrime37 ┃ 159700981134 │ ║ 42 │ Yes ║ 192137064411 │ │ |
|
* ├─────────────────────╂───────────────┼─────╫───────────────┼─────╫───────────────┼─────┤ |
|
* │ NbLen4 ┃ 1245225811379 │ ║ 1079924085291 │ ║ 42 │ Yes │ |
|
* ╰─────────────────────┸───────────────┴──╥──╨───────────────┴──╥──╨───────────────┴──╥──╯ |
|
* ╚══▷ Column of ◁══╝ ◁══════╝ |
|
* "Is Correct?" |
|
* |
|
* ``` |
|
* |
|
* @param {String} str Previously generated UID |
|
* @returns {Number} the original number id |
|
*/ |
|
function nblDecodeUID(str) { |
|
// console.log({ DEFAULT_CONFIG, cfg, str}) |
|
let x = 0n; |
|
for (const ch of str) { |
|
x = x * cfg.baseNum + BigInt(cfg.alphabet.indexOf(ch)); |
|
} |
|
return Number((x * cfg.invPrime) % cfg.modulus); |
|
} |
|
|
|
/** |
|
* NibolsUID has two behaviors: Encode (default) or Decode. |
|
* |
|
* (encode, default): |
|
* From an positive integer identifier number, |
|
* get an fixed length unique string. |
|
* |
|
* (decode=true): |
|
* From a previously generated unique string, |
|
* get back the original positive integer identifier number. |
|
* |
|
* const id = NibolsUID("MYNV6QGB", true); // 7 |
|
* |
|
* @param {Number|String} n The ID to get the UID, or the UID to get the ID from (decode = true) |
|
* @param {Boolean} decode default FALSE, set to true to inverse the function and decode instead of encode. |
|
* @returns {String|Number} |
|
*/ |
|
function nblUID(n, decode = false) { |
|
return decode ? nblDecodeUID(n) : nblEncodeUID(n); |
|
} |
|
|
|
return { |
|
// function with original names |
|
|
|
nblUID, |
|
nblEncodeUID, |
|
nblDecodeUID, |
|
|
|
// aliases |
|
|
|
uid: nblUID, |
|
to: nblEncodeUID, |
|
from: nblDecodeUID, |
|
encode: nblEncodeUID, |
|
decode: nblDecodeUID, |
|
}; |
|
} |
|
|
|
// Extended Euclidean Algorithm |
|
function modInverse(a, m) { |
|
// console.log(`a, m: ${a}, ${m}`); |
|
|
|
let m0 = m, x0 = 0n, x1 = 1n; |
|
let b = a % m; |
|
while (b > 1n) { |
|
const q = b / m; |
|
[b, m] = [m, b % m]; |
|
[x0, x1] = [x1 - q * x0, x0]; |
|
} |
|
const r = x1 < 0n ? x1 + m0 : x1; |
|
return BigInt(r); |
|
} |
|
|
|
function CreateNibolsUID(config = {}) { |
|
let options = config; |
|
const cfg = {}; |
|
|
|
try { |
|
const test = String(options.alphabet) + String(options.size) + String(options.prime); |
|
} catch (e) { |
|
options = {}; |
|
} |
|
|
|
if (options.alphabet) { |
|
let valid = typeof options.alphabet === 'string' && options.alphabet.length > 10 |
|
const uniqueChars = new Set(options.alphabet.split('')).size; |
|
const inputLen = options.alphabet.length; |
|
if (uniqueChars < inputLen) { valid = false; } |
|
|
|
if (valid) { |
|
cfg.alphabet = options.alphabet; |
|
} else { |
|
throw new Error(`NibolsUID: The provided set of symbols (alphabet options) used to make the UID must be at least larger that the numbers available in base 10 maths.`); |
|
} |
|
} |
|
|
|
if (options.size) { |
|
if (Number.isInteger(options.size) && options.size > 3) { |
|
if (options.size > 13) { |
|
throw new Error(`NibolsUID: The desired size of ${options.size} is beyond the maximum supported size of 13.`) |
|
} |
|
cfg.size = BigInt(options.size); |
|
} else { |
|
throw new Error(`NibolsUID: The desired length (size option) provided is either something not numerical, or less than 3.`); |
|
} |
|
} else { |
|
cfg.size = 8n; |
|
} |
|
|
|
if (options.prime) { |
|
if (Number.isInteger(options.prime) && options.prime > 1) { |
|
cfg.prime = BigInt(options.prime); |
|
} else { |
|
throw new Error(`NibolsUID: Invalid prime option, ensure it is at least a positive integer number.`); |
|
} |
|
} else { |
|
// console.log(cfg.size, goodPrimes30[cfg.size]) |
|
// cfg.prime = BigInt(goodPrimes30[cfg.size]); |
|
} |
|
|
|
return NibolsUIDFactory(cfg); |
|
} |
|
|
|
return CreateNibolsUID; |
|
})() |