From 73dce2665b9452635692214c0652f4dd934952b3 Mon Sep 17 00:00:00 2001 From: Yarchik Date: Mon, 22 Jun 2026 19:50:14 +0100 Subject: [PATCH] fix: preserve exact value of large integers on round-trip Integers are encoded via Number#toString(36) and decoded via parseInt(_, 36); both accumulate in a double, so integers beyond 2^53 lose precision and break the documented round-trip contract: unpack(pack({n: 1e21})).n // 1.0000000000000001e21, not 1e21 Route the integer codec through BigInt, which is exact. Dictionary indices are small, so the BigInt path produces identical output for them, and the float path (String(item)) is untouched. Added a round-trip test for values beyond 2^53. --- index.js | 17 +++++++++++++++-- test/test.js | 4 ++++ 2 files changed, 19 insertions(+), 2 deletions(-) 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); + }); }); });