diff --git a/navigator/src/functions/szudzikPair.js b/navigator/src/functions/szudzikPair.js new file mode 100644 index 0000000..041e05a --- /dev/null +++ b/navigator/src/functions/szudzikPair.js @@ -0,0 +1,111 @@ +/** + * Perform isqrt on BigInt + * + * @param {BigInt} s - Square + * @returns {BigInt} - Integer square root + */ +const bigIntSqrt = (s) => { + if (s < 0n) + throw new Error("isqrt of negative number is not allowed"); + + if (s < 2) + return s; + + if (s <= Number.MAX_SAFE_INTEGER) + return BigInt(Math.floor(Math.sqrt(Number(s)))); + + let x0, x1; + x0 = s / 2n; // initial estimate + + x1 = (x0 + s / x0) / 2n; + while (x1 < x0) { + x0 = x1; + x1 = (x0 + s / x0) / 2n; + } + return x0; +} + +/** + * Generate an unsigned 32 bit hash of a string + * + * @param {String} string - string to hash + * @returns {Number} - hash of string + */ +const hashString = (string) => { + let i = 0, digest = 0, char = 0, length = string.length; + for (; i < length; i++) { + char = string.charCodeAt(i); + digest = ((digest * 31) + char) | 0; + } + if (digest < 0) + digest += 2**32 + return digest; +} + +/** + * Encode two BigInt values to one unique BigInt value + * + * @param {BigInt} k0 - first pair element + * @param {BigInt} k1 - second pair element + * @returns {BigInt} - The encoded result + */ +const szudzikPair2 = (k0, k1) => + k0 > k1 + ? k0 ** 2n + k1 + : k1 ** 2n + k1 + k0 + +/** + * Decode one unique BigInt value to its original pair + * + * @param {BigInt} z - Encoded value + * @returns {BigInt[]} - Decoded pair + */ +const szudzikUnpair2 = (z) => { + const r = bigIntSqrt(z); + return (z - r ** 2n) < r + ? [r, z - r ** 2n] + : [z - r ** 2n - r, r]; +} + +/** + * Encode an arbitrary number of BigInt values to one unique BigInt value + * + * @param {...BigInt|BigInt[]} args - tuple to encode + * @returns {BigInt} - Encoded value + */ +function szudzikPair(...args) { + let k0, k1; + const k = [...(Array.isArray(args[0]) ? args[0] : args)].map(v => typeof v === 'string' ? BigInt(hashString(v)) : BigInt(v)); + if (k.length == 2) + return szudzikPair2(...k); + while (k.length >= 2) { + k0 = k.shift(); + k1 = k[0]; + k[0] = szudzikPair2(k0, k1); + } + return k[0]; +} + +/** + * Decode one unique BigInt value to its original tuple of length n + * + * @param {BigInt} z - Encoded value + * @param {Number} n - Size of decoded tuple + * @returns {BigInt[]} - Decoded tuple + */ +function szudzikUnpair(z, n = 2) { + const k = [BigInt(z)]; + for (let i = 0; i < n - 1; i++) { + k.unshift(...szudzikUnpair2(k.shift())); + } + return k; +} + +export { + szudzikPair, + szudzikUnpair, + szudzikPair2, + szudzikUnpair2, + bigIntSqrt, + hashString, +} diff --git a/navigator/src/functions/szudzikPair.test.js b/navigator/src/functions/szudzikPair.test.js new file mode 100644 index 0000000..6161b6d --- /dev/null +++ b/navigator/src/functions/szudzikPair.test.js @@ -0,0 +1,139 @@ +import { + szudzikPair, + szudzikUnpair, + szudzikPair2, + szudzikUnpair2, + bigIntSqrt, + hashString, +} from "./szudzikPair"; + +describe('Szudzik Pairing', () => { + describe('Internal functions', () => { + describe('bigIntSqrt', () => { + it('can accurately calculate a small square root', () => { + expect(bigIntSqrt(25n)).toBe(5n); + expect(bigIntSqrt(100n)).toBe(10n); + expect(bigIntSqrt(16n)).toBe(4n); + }); + + it('can match Math.floor(Math.sqrt())', () => { + const inputs = [...Array(1000).keys()]; + for (const input of inputs) { + expect(bigIntSqrt(BigInt(input)) == Math.floor(Math.sqrt(input))).toBe(true); + } + }); + + it('can calculate very large square roots', () => { + const inputs = [...Array(1000).keys()].map(BigInt).map(x => x + BigInt(Number.MAX_SAFE_INTEGER.toString(10))); + for (const input of inputs) { + const s = input ** 2n; + expect(bigIntSqrt(s)).toBe(input); + } + }) + }); + + describe('hashString', () => { + it('can generate positive unique hashes for host names', () => { + const hosts = [ + 'localhost', + 'osd1', + 'osd2', + 'osd3', + 'fsgw1', + 'fsgw2', + 'fsgw3', + 'ubuntu', + 'rocky', + 'server', + 'storinator', + 'bartholomew', + ]; + const hashes = hosts.map(host => hashString(host)); + expect((new Set(hashes)).size).toEqual(hosts.length); + expect(hashes.every(h => h >= 0)).toEqual(true); + }); + }); + + describe('szudzikPair2', () => { + it('can generate an encoding from two numbers', () => { + expect(szudzikPair2(1n, 2n)).toBe(7n); + expect(szudzikPair2(2n, 1n)).toBe(5n); + expect(szudzikPair2(10n, 25n)).toBe(660n); + }); + + it('can generate an encoding from two > MAX_SAFE_INT numbers', () => { + expect(szudzikPair2(BigInt(Number.MAX_SAFE_INTEGER) + 5n, BigInt(Number.MAX_SAFE_INTEGER) + 10n)).toBe(81129638414606861839774099963998n); + }); + }); + + describe('szudzikUnpair2', () => { + it('can decode into the original pair', () => { + expect(szudzikUnpair2(7n)).toEqual([1n, 2n]); + expect(szudzikUnpair2(5n)).toEqual([2n, 1n]); + expect(szudzikUnpair2(660n)).toEqual([10n, 25n]); + }); + + it('can decode into the original pair for > MAX_SAFE_INT', () => { + expect(szudzikUnpair2(81129638414606861839774099963998n)).toEqual([BigInt(Number.MAX_SAFE_INTEGER) + 5n, BigInt(Number.MAX_SAFE_INTEGER) + 10n]); + }); + }); + }); + + describe('API', () => { + describe('szudzikPair', () => { + it('can generate an encoding from two numbers', () => { + expect(szudzikPair(1n, 2n)).toBe(7n); + expect(szudzikPair(2n, 1n)).toBe(5n); + expect(szudzikPair(10n, 25n)).toBe(660n); + }); + + it('can generate an encoding from two > MAX_SAFE_INT numbers', () => { + expect(szudzikPair(BigInt(Number.MAX_SAFE_INTEGER) + 5n, BigInt(Number.MAX_SAFE_INTEGER) + 10n)).toBe(81129638414606861839774099963998n); + }); + }); + + describe('szudzikUnpair', () => { + it('can decode into the original pair', () => { + expect(szudzikUnpair(7n, 2)).toEqual([1n, 2n]); + expect(szudzikUnpair(5n, 2)).toEqual([2n, 1n]); + expect(szudzikUnpair(660n, 2)).toEqual([10n, 25n]); + }); + + it('can decode into the original pair for > MAX_SAFE_INT', () => { + expect(szudzikUnpair(81129638414606861839774099963998n, 2)).toEqual([BigInt(Number.MAX_SAFE_INTEGER) + 5n, BigInt(Number.MAX_SAFE_INTEGER) + 10n]); + }); + }); + + describe('works with n > 2 (n = 4, 20^4 tuples)', () => { + const i = [...Array(20).keys()].map(BigInt); + const j = [...i]; + const k = [...i]; + const l = [...i]; + const tuples = []; + for (let i0 of i) { + for (let j0 of j) { + for (let k0 of k) { + for (let l0 of l) { + tuples.push([l0, k0, j0, i0]); + } + } + } + } + const encodings = tuples.map(szudzikPair); + + describe('has no collisions', () => { + expect((new Set(encodings)).size).toEqual(encodings.length); + }); + + describe('can be decoded', () => { + const decodings = encodings.map(e => szudzikUnpair(e, 4)); + expect(decodings).toEqual(tuples); + }); + }); + + describe('works with array or nargs', () => { + const input = [1n, 2n, 3n, 4n]; + expect(szudzikPair(input)).toEqual(szudzikPair(...input)); + }); + }) +});