From 90cd0a0c02bd15735eb06c54fc5ec18982b67089 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Wed, 12 Nov 2025 14:34:15 +0200 Subject: [PATCH 1/2] Add restrictDecimals() and formatDuration() implementations to new number formatting module --- lib/formatting/__tests__/numbers.test.ts | 40 ++++++++++++++++ lib/formatting/numbers.ts | 58 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 lib/formatting/__tests__/numbers.test.ts create mode 100644 lib/formatting/numbers.ts diff --git a/lib/formatting/__tests__/numbers.test.ts b/lib/formatting/__tests__/numbers.test.ts new file mode 100644 index 0000000..7f0f160 --- /dev/null +++ b/lib/formatting/__tests__/numbers.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; + +import { restrictDecimals, formatDuration } from '../numbers.js'; + +describe('formatting/numbers', () => { + describe('restrictDecimals()', () => { + it('should limit large floats to a specified number of places', () => { + // We are intentionally trying to test that it restricts the float properly, + // therefore the float warning rule is ignorable. + + const randomFloat = 1.5233219472835921359; + expect(restrictDecimals(randomFloat, 2)).toEqual(1.52); + expect(restrictDecimals(randomFloat, 1)).toEqual(1.5); + expect(restrictDecimals(randomFloat, 0)).toEqual(1); + }); + + it('should remove trailing zeroes', () => { + const randomFloat = 3.040203; + expect(restrictDecimals(randomFloat, 4)).toEqual(3.0402); + expect(restrictDecimals(randomFloat, 3)).toEqual(3.04); + expect(restrictDecimals(randomFloat, 1)).toEqual(3); + }); + }); + + describe('formatDuration()', () => { + it('should display the value in the most appropriate timescale', () => { + const microSecs = 0.095; + expect(formatDuration(microSecs)).toEqual('95μs'); + + const millisecs = 50; + expect(formatDuration(millisecs)).toEqual('50ms'); + + const seconds = 3000; + expect(formatDuration(seconds)).toEqual('3s'); + + const minutes = 65000; + expect(formatDuration(minutes)).toEqual('1m 5s'); + }); + }); +}); diff --git a/lib/formatting/numbers.ts b/lib/formatting/numbers.ts new file mode 100644 index 0000000..7c2eb02 --- /dev/null +++ b/lib/formatting/numbers.ts @@ -0,0 +1,58 @@ +/** + * Restricts the given value to a certain number of decimal places, but does NOT + * force that number of places like `.toFixed()` does. Trailing 0's will be removed. + */ +export function restrictDecimals(value: number, maxDecimalPlaces: number): number { + if (maxDecimalPlaces < 0) return value; + // When 0, dont use .toFixed since it rounds + if (maxDecimalPlaces === 0) { + return Math.floor(value); + } + + // Force to fixed -> Convert back to number to remove trailing 0's. + return Number(value.toFixed(maxDecimalPlaces)); +} + +/** + * Takes a given duration in milliseconds and formats it into a more readable + * string value suitable for logging or display. + * + * Determines what timescale to use in an opinionated fashion: + * + * - Below 0.1ms switches to microseconds for more readability. + * - Between 0.1ms - 999ms displays milliseconds. + * - Between 1s - 59s displays seconds. + * - Over 1m will display Xminutes Yseconds. + * + * The timescale choices try to enforce readability patterns that make it easier + * to visually understand how much time something takes. Large values that could + * be dislayed as a smaller value at a larger timescale are preferred, as they + * require less mental math to understand. + */ +export function formatDuration(durationMs: number): string { + // Show microseconds when below 0.1ms + if (0.1 > durationMs) { + return `${restrictDecimals(durationMs * 1000, 0).toString(10)}μs`; + } + + // Show milliseconds when below 1s + if (1000 > durationMs) { + return `${restrictDecimals(durationMs, 2).toString(10)}ms`; + } + + const totalSeconds = restrictDecimals(durationMs / 1000, 2); + // Handle just seconds + if (60 > totalSeconds) { + return `${totalSeconds.toString(10)}s`; + } + + // Handle seconds/minutes handling + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + + const formatted: string[] = []; + if (1 >= minutes) formatted.push(`${minutes.toString(10)}m`); + formatted.push(`${seconds.toString(10)}s`); + + return formatted.join(' '); +} From 23ad2c9a3c8f91bc743fb2519f384ec7149eb4f6 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Wed, 12 Nov 2025 14:36:47 +0200 Subject: [PATCH 2/2] Expose formatting modules via exports. --- .changeset/blue-snails-unite.md | 5 +++++ package.json | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/blue-snails-unite.md diff --git a/.changeset/blue-snails-unite.md b/.changeset/blue-snails-unite.md new file mode 100644 index 0000000..1d5dbf2 --- /dev/null +++ b/.changeset/blue-snails-unite.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Add new formatting modules including formatDuration() and restrictDecimals() functions. diff --git a/package.json b/package.json index 546c83e..646b621 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,10 @@ "./logging/node/*": { "default": "./dist/logging/node/*.js", "types": "./dist/logging/node/*.d.ts" + }, + "./formatting/*": { + "default": "./dist/formatting/*.js", + "types": "./dist/formatting/*.d.ts" } }, "files": [ @@ -61,4 +65,4 @@ "typescript": "^5.7.3", "vitest": "^3.0.6" } -} \ No newline at end of file +}