From 6c51534a70c772a790b1e19225c710c6e87cf51e Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch Date: Fri, 18 Sep 2026 02:06:46 +0000 Subject: [PATCH] fix: format() without scientific notation for parse() round-trips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Very small or very large finite values (e.g. 1e-7, 1e21) stringified as 1e-7ms / …e+…y, which parse() cannot read (returns NaN). Format the numeric component with toLocaleString so output stays decimal. Also align the parse() length error text with the actual 1..100 check. --- src/format.test.ts | 11 +++++++++++ src/index.ts | 42 +++++++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/format.test.ts b/src/format.test.ts index cdabb0f6..8748c2ec 100644 --- a/src/format.test.ts +++ b/src/format.test.ts @@ -259,3 +259,14 @@ describe('format(invalid inputs)', () => { }).toThrow(); }); }); + +describe('format avoids scientific notation', () => { + it('formats tiny millisecond values without scientific notation', () => { + expect(format(1e-7)).toBe('0.0000001ms'); + }); + + it('formats huge values without scientific notation', () => { + expect(format(1e21)).toBe('31688087814y'); + expect(format(1e21).includes('e')).toBe(false); + }); +}); diff --git a/src/index.ts b/src/index.ts index d50e3c77..5f18df3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,7 +71,7 @@ export function ms( export function parse(str: string): number { if (typeof str !== 'string' || str.length === 0 || str.length > 100) { throw new Error( - `Value provided to ms.parse() must be a string with length between 1 and 99. value=${JSON.stringify(str)}`, + `Value provided to ms.parse() must be a string with length between 1 and 100. value=${JSON.stringify(str)}`, ); } const match = @@ -157,33 +157,53 @@ export function parseStrict(value: StringValue): number { return parse(value); } + +/** + * Format a number without scientific notation so parse() can round-trip it. + */ +function numberToDecimalString(n: number): string { + if (Object.is(n, -0)) { + return '0'; + } + if (Number.isInteger(n)) { + return n.toLocaleString('en-US', { + useGrouping: false, + maximumFractionDigits: 0, + }); + } + return n.toLocaleString('en-US', { + useGrouping: false, + maximumSignificantDigits: 21, + }); +} + /** * Short format for `ms`. */ function fmtShort(ms: number): StringValue { const msAbs = Math.abs(ms); if (msAbs >= y) { - return `${Math.round(ms / y)}y`; + return `${numberToDecimalString(Math.round(ms / y))}y`; } if (msAbs >= mo) { - return `${Math.round(ms / mo)}mo`; + return `${numberToDecimalString(Math.round(ms / mo))}mo`; } if (msAbs >= w) { - return `${Math.round(ms / w)}w`; + return `${numberToDecimalString(Math.round(ms / w))}w`; } if (msAbs >= d) { - return `${Math.round(ms / d)}d`; + return `${numberToDecimalString(Math.round(ms / d))}d`; } if (msAbs >= h) { - return `${Math.round(ms / h)}h`; + return `${numberToDecimalString(Math.round(ms / h))}h`; } if (msAbs >= m) { - return `${Math.round(ms / m)}m`; + return `${numberToDecimalString(Math.round(ms / m))}m`; } if (msAbs >= s) { - return `${Math.round(ms / s)}s`; + return `${numberToDecimalString(Math.round(ms / s))}s`; } - return `${ms}ms`; + return `${numberToDecimalString(ms)}ms`; } /** @@ -212,7 +232,7 @@ function fmtLong(ms: number): StringValue { if (msAbs >= s) { return plural(ms, msAbs, s, 'second'); } - return `${ms} ms`; + return `${numberToDecimalString(ms)} ms`; } /** @@ -240,5 +260,5 @@ function plural( name: string, ): StringValue { const isPlural = msAbs >= n * 1.5; - return `${Math.round(ms / n)} ${name}${isPlural ? 's' : ''}` as StringValue; + return `${numberToDecimalString(Math.round(ms / n))} ${name}${isPlural ? 's' : ''}` as StringValue; }