diff --git a/src/dnum.ts b/src/dnum.ts index 94d7729..ccec518 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..f877663 100644 --- a/test/all.test.ts +++ b/test/all.test.ts @@ -998,6 +998,30 @@ 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 +1044,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])); });