import type { FinancialSurfaceKind, FinancialUnit } from './types'; export function asNumber(value: string | number | null | undefined) { if (value === null || value === undefined) { return 0; } const parsed = typeof value === 'number' ? value : Number(value); return Number.isFinite(parsed) ? parsed : 0; } type NumberScale = { divisor: number; suffix: string; }; export type NumberScaleUnit = 'thousands' | 'millions' | 'billions'; 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' } ]; const NUMBER_SCALE_UNITS: Record = { 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; }; 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 = {} ) { 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 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 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', currency: 'USD', maximumFractionDigits: 2 }).format(asNumber(value)); } export function formatPercent(value: string | number | null | undefined) { return `${asNumber(value).toFixed(2)}%`; } export function formatCompactCurrency(value: string | number | null | undefined) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 2 }).format(asNumber(value)); }