Skip to content

Commit

Permalink
Merge pull request #806 from HathorNetwork/master
Browse files Browse the repository at this point in the history
v2.0.1
  • Loading branch information
pedroferreira1 authored Dec 16, 2024
2 parents 3067c43 + 3304f67 commit 2c70266
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 38 deletions.
33 changes: 28 additions & 5 deletions __tests__/utils/bigint.test.js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,41 @@ const bigIntObjSchema = z.object({

describe('test JSONBigInt', () => {
test('should parse numbers', () => {
// Doubles should be parsed normally as JS Numbers.
expect(JSONBigInt.parse('123')).toStrictEqual(123);
expect(JSONBigInt.parse('123.456')).toStrictEqual(123.456);
expect(JSONBigInt.parse('1.0')).toStrictEqual(1);
expect(JSONBigInt.parse('1.000000000000')).toStrictEqual(1);
expect(JSONBigInt.parse('12345678901234567890')).toStrictEqual(12345678901234567890n);
expect(JSONBigInt.parse('12345678901234567890.000')).toStrictEqual(12345678901234567890n);
expect(JSONBigInt.parse('1e2')).toStrictEqual(100);
expect(JSONBigInt.parse('1E2')).toStrictEqual(100);

expect(() => JSONBigInt.parse('12345678901234567890.1')).toThrow(
Error('large float will lose precision! in "12345678901234567890.1"')
);
// This is 2**53-1 which is the MAX_SAFE_INTEGER, so it remains a Number, not a BigInt.
// And the analogous for MIN_SAFE_INTEGER.
expect(JSONBigInt.parse('9007199254740991')).toStrictEqual(9007199254740991);
expect(JSONBigInt.parse('-9007199254740991')).toStrictEqual(-9007199254740991);

// One more than the MAX_SAFE_INTEGER, so it becomes a BigInt. And the analogous for MIN_SAFE_INTEGER.
expect(JSONBigInt.parse('9007199254740992')).toStrictEqual(9007199254740992n);
expect(JSONBigInt.parse('-9007199254740992')).toStrictEqual(-9007199254740992n);

// This is just a random large value that would lose precision as a Number.
expect(JSONBigInt.parse('12345678901234567890')).toStrictEqual(12345678901234567890n);

// This is 2n**63n, which is the max output value.
expect(JSONBigInt.parse('9223372036854775808')).toStrictEqual(9223372036854775808n);

// This is the value 2n**63n would have when converted to a Number with loss of precision,
// and then some variation around it. Notice it's actually greater than 2n**63n.
expect(JSONBigInt.parse('9223372036854776000')).toStrictEqual(9223372036854776000n);
expect(JSONBigInt.parse('9223372036854775998')).toStrictEqual(9223372036854775998n);
expect(JSONBigInt.parse('9223372036854775999')).toStrictEqual(9223372036854775999n);
expect(JSONBigInt.parse('9223372036854776001')).toStrictEqual(9223372036854776001n);
expect(JSONBigInt.parse('9223372036854776002')).toStrictEqual(9223372036854776002n);

// This is 2n**63n - 800n and the value it would have when converted to a Number with loss of precision.
// Notice it becomes less than the original value.
expect(JSONBigInt.parse('9223372036854775008')).toStrictEqual(9223372036854775008n);
expect(JSONBigInt.parse('9223372036854775000')).toStrictEqual(9223372036854775000n);
});

test('should parse normal JSON', () => {
Expand Down
62 changes: 29 additions & 33 deletions src/utils/bigint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,43 @@ import { getDefaultLogger } from '../types';
export const JSONBigInt = {
/* eslint-disable @typescript-eslint/no-explicit-any */
parse(text: string): any {
function bigIntReviver(_key: string, value: any, context: { source: string }): any {
if (!Number.isInteger(value)) {
// No special handling needed for non-integer values.
return value;
}

let { source } = context;
if (source.includes('e') || source.includes('E')) {
// Values with exponential notation (such as 10e2) are always Number.
return value;
}
// @ts-expect-error TypeScript hasn't been updated with the `context` argument from Node v22.
return JSON.parse(text, this.bigIntReviver);
},

if (source.includes('.')) {
// If value is an integer and contains a '.', it must be like '123.0', so we extract the integer part only.
let zeroes: string;
[source, zeroes] = source.split('.');
stringify(value: any, space?: string | number): string {
return JSON.stringify(value, this.bigIntReplacer, space);
},

if (zeroes.split('').some(char => char !== '0')) {
// This case shouldn't happen but we'll prohibit it to be safe. For example, if the source is
// '12345678901234567890.1', JS will parse it as an integer with loss of precision, `12345678901234567000`.
throw Error(`large float will lose precision! in "${text}"`);
}
}
bigIntReviver(_key: string, value: any, context: { source: string }): any {
if (typeof value !== 'number') {
// No special handling needed for non-number values.
return value;
}

const bigIntValue = BigInt(source);
if (bigIntValue !== BigInt(value)) {
// If the parsed value is an integer and its BigInt representation is a different value,
// it means we lost precision, so we return the BigInt.
try {
const bigIntValue = BigInt(context.source);
if (bigIntValue < Number.MIN_SAFE_INTEGER || bigIntValue > Number.MAX_SAFE_INTEGER) {
// We only return the value as a BigInt if it's in the unsafe range.
return bigIntValue;
}

// No special handling needed.
// Otherwise, we can keep it as a Number.
return value;
} catch (e) {
if (
e instanceof SyntaxError &&
e.message === `Cannot convert ${context.source} to a BigInt`
) {
// When this error happens, it means the number cannot be converted to a BigInt,
// so it's a double, for example '123.456' or '1e2'.
return value;
}
// This should never happen, any other error thrown by BigInt() is unexpected.
const logger = getDefaultLogger();
logger.error(`unexpected error in bigIntReviver: ${e}`);
throw e;
}

// @ts-expect-error TypeScript hasn't been updated with the `context` argument from Node v22.
return JSON.parse(text, bigIntReviver);
},

stringify(value: any, space?: string | number): string {
return JSON.stringify(value, this.bigIntReplacer, space);
},

bigIntReplacer(_key: string, value_: any): any {
Expand Down

0 comments on commit 2c70266

Please sign in to comment.