diff --git a/index.js b/index.js index 6a5f988..3d6db1a 100644 --- a/index.js +++ b/index.js @@ -33,8 +33,21 @@ const _decode = (str) => { })[a]); }; -const _base10To36 = (number) => Number.prototype.toString.call(number, 36).toUpperCase(); -const _base36To10 = (number) => parseInt(number, 36); +// Encode/decode integers through BigInt so values beyond 2^53 are exact. +// `Number#toString(36)` and `parseInt(_, 36)` both accumulate in a double and +// lose precision for large integers, which broke the round-trip contract +// (e.g. 1e21 came back as 1.0000000000000001e21). Indices stay small, so the +// BigInt path is equivalent for them. +const _base10To36 = (number) => BigInt(number).toString(36).toUpperCase(); +const _base36To10 = (str) => { + const negative = str.charAt(0) === '-'; + const digits = negative ? str.slice(1) : str; + let acc = 0n; + for (const ch of digits) { + acc = acc * 36n + BigInt(parseInt(ch, 36)); + } + return Number(negative ? -acc : acc); +}; const pack = (json, options = {}) => { const verbose = options.verbose || false; diff --git a/test/test.js b/test/test.js index e765ddb..b8236f2 100644 --- a/test/test.js +++ b/test/test.js @@ -87,6 +87,10 @@ describe('jsonpack', () => { const obj = { space: 'hello world', plus: 'a+b', pipe: 'a|b', caret: 'a^b', percent: '50%' }; assert.deepEqual(roundTrip(obj), obj); }); + it('large integers beyond 2^53', () => { + const obj = { a: 1e21, b: 1e23, c: 1.5e300, d: -1.5e300, e: Number.MAX_SAFE_INTEGER }; + assert.deepEqual(roundTrip(obj), obj); + }); }); });