From a5136b361f9ae534ee676742ec6c5b4b2327cd14 Mon Sep 17 00:00:00 2001 From: pavitra-infocusp <146841615+pavitra-infocusp@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:30:44 +0000 Subject: [PATCH 1/2] feat: add locale-aware number parsing and tests for formatted strings --- src/dnum.ts | 20 ++++++++++++++++++++ test/all.test.ts | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/dnum.ts b/src/dnum.ts index 94d7729..ff359b0 100644 --- a/src/dnum.ts +++ b/src/dnum.ts @@ -27,6 +27,22 @@ export function isDnum(value: unknown): value is Dnum { ); } +/** + * Locale-aware number parser that can handle strings with thousands separator. + * + * @param strNum formatted string with thousands separator to parse + * @returns parsed number + * @example parseLocaleNumber("1,234.56") === "1234.56" + * @link https://stackoverflow.com/a/51157574 + */ +function parseLocaleNumber(strNum: string): string { + const decSep = (1.1).toLocaleString().substring(1, 2); + const formatted = strNum + .replace(new RegExp(`([${decSep}])(?=.*\\1)`, 'g'), '') + .replace(new RegExp(`[^-0-9${decSep}]`, 'g'), ''); + return formatted.replace(decSep, '.'); +} + // Matches: // - whole numbers (123) // - decimal numbers (1.23, .23) @@ -49,6 +65,10 @@ export function from( value = fromExponential(value); } + if (value.includes(",")) { + value = parseLocaleNumber(value) + } + if (!value.match(NUM_RE)) { throw new Error(`dnum: incorrect number (${value})`); } diff --git a/test/all.test.ts b/test/all.test.ts index 58c576c..acbe6bc 100644 --- a/test/all.test.ts +++ b/test/all.test.ts @@ -998,6 +998,24 @@ describe("from()", () => { expect(from(".29387", 18)).toEqual([293870000000000000n, 18]); expect(from("-.29387", 18)).toEqual([-293870000000000000n, 18]); }); + it("accepts formatted strings with thousands separator", () => { + expect(from("12,345.29387", 18)).toEqual([12345_293870000000000000n, 18]); + expect(from("12,345.29387", 2)).toEqual([12345_29n, 2]); + expect(from("12,345.29387", 0)).toEqual([12345n, 0]); + expect(from("-12,345.29387", 0)).toEqual([-12345n, 0]); + expect(from(".29,387", 18)).toEqual([293870000000000000n, 18]); + expect(from("-.29,387", 18)).toEqual([-293870000000000000n, 18]); + // TODO: figure out how to programmatically run the tests in different locales + // I have only found a way to do that by setting the `LC_ALL='de-DE.UTF-8'` env var + }); + it("accepts formatted strings with thousands separator in scientific notation", () => { + expect(from(12345.29387 * 10 ** 21, 5)).toEqual([12345293870000000000000000_00000n, 5]); + expect(from(-12345.29387 * 10 ** 21, 5)).toEqual([-12345293870000000000000000_00000n, 5]); + // NOTE: these cases currently fail because of floating point precision issues + // > .29387 * 10 ** 21 === 293870000000000030000 + // expect(from(.29387 * 10 ** 21, 5)).toEqual([293870000000000000000_00000n, 5]); + // expect(from(-.29387 * 10 ** 21, 5)).toEqual([-293870000000000000000_00000n, 5]); + }); it("works with Dnums", () => { expect(from([12345n, 2], 2)).toEqual([12345n, 2]); expect(from([12345n, 2], 4)).toEqual([1234500n, 4]); @@ -1020,6 +1038,7 @@ describe("from()", () => { }); it("throws with incorrect values", () => { expect(from(10 ** 21)).toEqual([10n ** 21n, 0]); + // expected because it is not a valid number in any locale expect(() => from("3298.987.32", 18)) .toThrowErrorMatchingSnapshot(JSON.stringify(["3298.987.32", 18])); }); From 4a295649e6a6cc6a5363afe39dd87b0372c29e8b Mon Sep 17 00:00:00 2001 From: pavitra-infocusp <146841615+pavitra-infocusp@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:44:50 +0000 Subject: [PATCH 2/2] chore: fmt --- src/dnum.ts | 8 ++++---- test/all.test.ts | 10 ++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/dnum.ts b/src/dnum.ts index ff359b0..ccec518 100644 --- a/src/dnum.ts +++ b/src/dnum.ts @@ -38,9 +38,9 @@ export function isDnum(value: unknown): value is Dnum { function parseLocaleNumber(strNum: string): string { const decSep = (1.1).toLocaleString().substring(1, 2); const formatted = strNum - .replace(new RegExp(`([${decSep}])(?=.*\\1)`, 'g'), '') - .replace(new RegExp(`[^-0-9${decSep}]`, 'g'), ''); - return formatted.replace(decSep, '.'); + .replace(new RegExp(`([${decSep}])(?=.*\\1)`, "g"), "") + .replace(new RegExp(`[^-0-9${decSep}]`, "g"), ""); + return formatted.replace(decSep, "."); } // Matches: @@ -66,7 +66,7 @@ export function from( } if (value.includes(",")) { - value = parseLocaleNumber(value) + value = parseLocaleNumber(value); } if (!value.match(NUM_RE)) { diff --git a/test/all.test.ts b/test/all.test.ts index acbe6bc..f877663 100644 --- a/test/all.test.ts +++ b/test/all.test.ts @@ -1009,8 +1009,14 @@ describe("from()", () => { // I have only found a way to do that by setting the `LC_ALL='de-DE.UTF-8'` env var }); it("accepts formatted strings with thousands separator in scientific notation", () => { - expect(from(12345.29387 * 10 ** 21, 5)).toEqual([12345293870000000000000000_00000n, 5]); - expect(from(-12345.29387 * 10 ** 21, 5)).toEqual([-12345293870000000000000000_00000n, 5]); + expect(from(12345.29387 * 10 ** 21, 5)).toEqual([ + 12345293870000000000000000_00000n, + 5, + ]); + expect(from(-12345.29387 * 10 ** 21, 5)).toEqual([ + -12345293870000000000000000_00000n, + 5, + ]); // NOTE: these cases currently fail because of floating point precision issues // > .29387 * 10 ** 21 === 293870000000000030000 // expect(from(.29387 * 10 ** 21, 5)).toEqual([293870000000000000000_00000n, 5]);