Improve financial statement value formatting

This commit is contained in:
2026-03-09 18:58:15 -04:00
parent fae8c54121
commit fa2de3e259
3 changed files with 306 additions and 88 deletions

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from 'bun:test';
import { formatCurrencyByScale, formatScaledNumber } from './format';
import {
formatCurrencyByScale,
formatFinancialStatementValue,
formatScaledNumber
} from './format';
describe('formatScaledNumber', () => {
it('keeps values below one thousand unscaled', () => {
@@ -45,3 +49,59 @@ describe('formatCurrencyByScale', () => {
expect(formatCurrencyByScale(-2_500_000, 'millions')).toBe('-$2.5M');
});
});
describe('formatFinancialStatementValue', () => {
it('renders null statement values as em dashes', () => {
expect(formatFinancialStatementValue({
value: null,
unit: 'currency',
scale: 'millions',
rowKey: 'revenue',
surfaceKind: 'income_statement'
})).toBe('—');
});
it('formats scaled currency values without a currency symbol', () => {
expect(formatFinancialStatementValue({
value: 15_940_899_000,
unit: 'currency',
scale: 'millions',
rowKey: 'revenue',
surfaceKind: 'income_statement'
})).toBe('15,940.9');
expect(formatFinancialStatementValue({
value: 1_100_000_000,
unit: 'currency',
scale: 'millions',
rowKey: 'long_term_debt_issued',
surfaceKind: 'cash_flow_statement'
})).toBe('1,100');
});
it('keeps extra precision for per-share rows', () => {
expect(formatFinancialStatementValue({
value: 14.64,
unit: 'currency',
scale: 'millions',
rowKey: 'diluted_eps',
surfaceKind: 'income_statement'
})).toBe('14.64');
});
it('formats statement percents and ratios with trimmed precision', () => {
expect(formatFinancialStatementValue({
value: 0.233,
unit: 'percent',
scale: 'millions',
rowKey: 'effective_tax_rate',
surfaceKind: 'income_statement'
})).toBe('23.3%');
expect(formatFinancialStatementValue({
value: 1.5,
unit: 'ratio',
scale: 'millions',
rowKey: 'debt_to_equity',
surfaceKind: 'ratios'
})).toBe('1.5x');
});
});

View File

@@ -1,3 +1,8 @@
import type {
FinancialSurfaceKind,
FinancialUnit
} from './types';
export function asNumber(value: string | number | null | undefined) {
if (value === null || value === undefined) {
return 0;
@@ -37,6 +42,37 @@ type FormatScaledCurrencyOptions = {
maximumFractionDigits?: number;
};
type FormatFinancialStatementValueInput = {
value: string | number | null | undefined;
unit: FinancialUnit;
scale: NumberScaleUnit;
rowKey?: string | null;
surfaceKind?: FinancialSurfaceKind | null;
isPercentChange?: boolean;
isCommonSize?: boolean;
};
function isPerShareRowKey(rowKey: string | null | undefined) {
if (!rowKey) {
return false;
}
return rowKey.includes('eps') || rowKey.includes('per_share');
}
function formatPlainNumber(
value: number,
options: {
minimumFractionDigits?: number;
maximumFractionDigits?: number;
} = {}
) {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: options.minimumFractionDigits ?? 0,
maximumFractionDigits: options.maximumFractionDigits ?? 1
}).format(value);
}
export function formatScaledNumber(
value: string | number | null | undefined,
options: FormatScaledNumberOptions = {}
@@ -99,6 +135,40 @@ export function formatCurrencyByScale(
return `${formatted}${suffix}`;
}
export function formatFinancialStatementValue(input: FormatFinancialStatementValueInput) {
if (input.value === null || input.value === undefined) {
return '—';
}
const numeric = typeof input.value === 'number' ? input.value : Number(input.value);
if (!Number.isFinite(numeric)) {
return '—';
}
if (input.isPercentChange || input.isCommonSize || input.unit === 'percent') {
return `${formatPlainNumber(numeric * 100, { minimumFractionDigits: 0, maximumFractionDigits: 1 })}%`;
}
if (input.unit === 'ratio') {
return `${formatPlainNumber(numeric, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}x`;
}
if (input.unit === 'currency' && isPerShareRowKey(input.rowKey)) {
return formatPlainNumber(numeric, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
if (input.unit === 'currency') {
const scaled = numeric / NUMBER_SCALE_UNITS[input.scale].divisor;
return formatPlainNumber(scaled, { minimumFractionDigits: 0, maximumFractionDigits: 1 });
}
if (input.unit === 'shares' || input.unit === 'count') {
return formatPlainNumber(numeric, { minimumFractionDigits: 0, maximumFractionDigits: 0 });
}
return formatPlainNumber(numeric, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
}
export function formatCurrency(value: string | number | null | undefined) {
return new Intl.NumberFormat('en-US', {
style: 'currency',