Add scaled number formatter for K/M/B values

This commit is contained in:
2026-03-01 19:17:49 -05:00
parent 6cef5f95ad
commit 209b650658
2 changed files with 86 additions and 0 deletions

29
lib/format.test.ts Normal file
View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from 'bun:test';
import { formatScaledNumber } from './format';
describe('formatScaledNumber', () => {
it('keeps values below one thousand unscaled', () => {
expect(formatScaledNumber(950)).toBe('950');
expect(formatScaledNumber(950.44, { maximumFractionDigits: 2 })).toBe('950.44');
});
it('scales thousands with K suffix', () => {
expect(formatScaledNumber(12_345)).toBe('12.3K');
});
it('scales millions with M suffix', () => {
expect(formatScaledNumber(3_250_000)).toBe('3.3M');
});
it('scales billions with B suffix', () => {
expect(formatScaledNumber(7_500_000_000)).toBe('7.5B');
});
it('preserves negative sign when scaled', () => {
expect(formatScaledNumber(-2_750_000)).toBe('-2.8M');
});
it('promotes rounded values to the next scale when needed', () => {
expect(formatScaledNumber(999_950)).toBe('1M');
});
});

View File

@@ -7,6 +7,63 @@ export function asNumber(value: string | number | null | undefined) {
return Number.isFinite(parsed) ? parsed : 0;
}
type NumberScale = {
divisor: number;
suffix: string;
};
const NUMBER_SCALES: NumberScale[] = [
{ divisor: 1, suffix: '' },
{ divisor: 1_000, suffix: 'K' },
{ divisor: 1_000_000, suffix: 'M' },
{ divisor: 1_000_000_000, suffix: 'B' }
];
type FormatScaledNumberOptions = {
minimumFractionDigits?: number;
maximumFractionDigits?: number;
};
export function formatScaledNumber(
value: string | number | null | undefined,
options: FormatScaledNumberOptions = {}
) {
const {
minimumFractionDigits = 0,
maximumFractionDigits = 1
} = options;
const numeric = asNumber(value);
const absolute = Math.abs(numeric);
let scaleIndex = 0;
if (absolute >= 1_000_000_000) {
scaleIndex = 3;
} else if (absolute >= 1_000_000) {
scaleIndex = 2;
} else if (absolute >= 1_000) {
scaleIndex = 1;
}
let scaledAbsolute = absolute / NUMBER_SCALES[scaleIndex].divisor;
if (
Number(scaledAbsolute.toFixed(maximumFractionDigits)) >= 1_000
&& scaleIndex < NUMBER_SCALES.length - 1
) {
scaleIndex += 1;
scaledAbsolute = absolute / NUMBER_SCALES[scaleIndex].divisor;
}
const scaled = numeric < 0 ? -scaledAbsolute : scaledAbsolute;
const formatted = new Intl.NumberFormat('en-US', {
minimumFractionDigits,
maximumFractionDigits
}).format(scaled);
return `${formatted}${NUMBER_SCALES[scaleIndex].suffix}`;
}
export function formatCurrency(value: string | number | null | undefined) {
return new Intl.NumberFormat('en-US', {
style: 'currency',