Add K/M/B scale toggles for financial displays
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { formatScaledNumber } from './format';
|
||||
import { formatCurrencyByScale, formatScaledNumber } from './format';
|
||||
|
||||
describe('formatScaledNumber', () => {
|
||||
it('keeps values below one thousand unscaled', () => {
|
||||
@@ -27,3 +27,21 @@ describe('formatScaledNumber', () => {
|
||||
expect(formatScaledNumber(999_950)).toBe('1M');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatCurrencyByScale', () => {
|
||||
it('formats values in thousands', () => {
|
||||
expect(formatCurrencyByScale(12_345, 'thousands')).toBe('$12.3K');
|
||||
});
|
||||
|
||||
it('formats values in millions', () => {
|
||||
expect(formatCurrencyByScale(12_345_678, 'millions')).toBe('$12.3M');
|
||||
});
|
||||
|
||||
it('formats values in billions', () => {
|
||||
expect(formatCurrencyByScale(12_345_678_901, 'billions')).toBe('$12.3B');
|
||||
});
|
||||
|
||||
it('keeps sign for negative values', () => {
|
||||
expect(formatCurrencyByScale(-2_500_000, 'millions')).toBe('-$2.5M');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ type NumberScale = {
|
||||
suffix: string;
|
||||
};
|
||||
|
||||
export type NumberScaleUnit = 'thousands' | 'millions' | 'billions';
|
||||
|
||||
const NUMBER_SCALES: NumberScale[] = [
|
||||
{ divisor: 1, suffix: '' },
|
||||
{ divisor: 1_000, suffix: 'K' },
|
||||
@@ -19,11 +21,22 @@ const NUMBER_SCALES: NumberScale[] = [
|
||||
{ divisor: 1_000_000_000, suffix: 'B' }
|
||||
];
|
||||
|
||||
const NUMBER_SCALE_UNITS: Record<NumberScaleUnit, NumberScale> = {
|
||||
thousands: { divisor: 1_000, suffix: 'K' },
|
||||
millions: { divisor: 1_000_000, suffix: 'M' },
|
||||
billions: { divisor: 1_000_000_000, suffix: 'B' }
|
||||
};
|
||||
|
||||
type FormatScaledNumberOptions = {
|
||||
minimumFractionDigits?: number;
|
||||
maximumFractionDigits?: number;
|
||||
};
|
||||
|
||||
type FormatScaledCurrencyOptions = {
|
||||
minimumFractionDigits?: number;
|
||||
maximumFractionDigits?: number;
|
||||
};
|
||||
|
||||
export function formatScaledNumber(
|
||||
value: string | number | null | undefined,
|
||||
options: FormatScaledNumberOptions = {}
|
||||
@@ -64,6 +77,28 @@ export function formatScaledNumber(
|
||||
return `${formatted}${NUMBER_SCALES[scaleIndex].suffix}`;
|
||||
}
|
||||
|
||||
export function formatCurrencyByScale(
|
||||
value: string | number | null | undefined,
|
||||
scale: NumberScaleUnit,
|
||||
options: FormatScaledCurrencyOptions = {}
|
||||
) {
|
||||
const {
|
||||
minimumFractionDigits = 0,
|
||||
maximumFractionDigits = 1
|
||||
} = options;
|
||||
const { divisor, suffix } = NUMBER_SCALE_UNITS[scale];
|
||||
const scaled = asNumber(value) / divisor;
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits
|
||||
}).format(scaled);
|
||||
|
||||
return `${formatted}${suffix}`;
|
||||
}
|
||||
|
||||
export function formatCurrency(value: string | number | null | undefined) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
|
||||
Reference in New Issue
Block a user