TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { CurrencyCode } from '@rareindex/shared';23/** Display formatting shared by server and client components (no Node-only imports). */45export function fmtMoney(amount: number | null | undefined, currency: CurrencyCode | string = 'USD', opts: { compact?: boolean; digits?: number } = {}): string {6 if (amount === null || amount === undefined || !Number.isFinite(amount)) return '—';7 const zero = currency === 'JPY' || currency === 'KRW';8 const digits = opts.digits ?? (zero ? 0 : Math.abs(amount) >= 1000 ? 0 : 2);9 try {10 return new Intl.NumberFormat('en-US', { style: 'currency', currency, notation: opts.compact ? 'compact' : 'standard', maximumFractionDigits: digits, minimumFractionDigits: opts.compact ? 0 : digits }).format(amount);11 } catch {12 return `${amount.toFixed(digits)} ${currency}`;13 }14}1516export function fmtPct(value: number | null | undefined, digits = 2, signed = true): string {17 if (value === null || value === undefined || !Number.isFinite(value)) return '—';18 const sign = signed && value > 0 ? '+' : '';19 return `${sign}${(value * 100).toFixed(digits)}%`;20}2122export function fmtNum(value: number | null | undefined, opts: { compact?: boolean; digits?: number } = {}): string {23 if (value === null || value === undefined || !Number.isFinite(value)) return '—';24 return new Intl.NumberFormat('en-US', { notation: opts.compact ? 'compact' : 'standard', maximumFractionDigits: opts.digits ?? (opts.compact ? 1 : 0) }).format(value);25}2627export function fmtDate(d: Date | string | null | undefined, opts: { month?: 'short' | 'long' | 'numeric'; year?: boolean; time?: boolean } = {}): string {28 if (!d) return '—';29 const date = typeof d === 'string' ? new Date(d.length === 10 ? `${d}T00:00:00Z` : d) : d;30 if (Number.isNaN(date.getTime())) return '—';31 return new Intl.DateTimeFormat('en-US', { month: opts.month ?? 'short', day: 'numeric', year: opts.year === false ? undefined : 'numeric', ...(opts.time ? { hour: 'numeric', minute: '2-digit' } : {}), timeZone: 'UTC' }).format(date);32}3334export function fmtRelative(d: Date | string | null | undefined): string {35 if (!d) return '—';36 const date = typeof d === 'string' ? new Date(d) : d;37 const diff = Date.now() - date.getTime();38 const abs = Math.abs(diff);39 const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });40 const units: Array<[number, Intl.RelativeTimeFormatUnit]> = [41 [60_000, 'second'],42 [3_600_000, 'minute'],43 [86_400_000, 'hour'],44 [604_800_000, 'day'],45 [2_629_800_000, 'week'],46 [31_557_600_000, 'month'],47 [Infinity, 'year'],48 ];49 let prevMs = 1;50 for (const [ms, unit] of units) {51 if (abs < ms) return rtf.format(Math.round(-diff / prevMs), unit);52 prevMs = ms;53 }54 return fmtDate(date);55}5657export function deltaClass(value: number | null | undefined): string {58 if (value === null || value === undefined || !Number.isFinite(value) || Math.abs(value) < 0.00005) return 'text-flat';59 return value > 0 ? 'text-gain' : 'text-loss';60}6162export function confidenceLabel(c: number | null | undefined): 'High' | 'Medium' | 'Low' | 'Insufficient' {63 if (c === null || c === undefined) return 'Insufficient';64 if (c >= 0.75) return 'High';65 if (c >= 0.5) return 'Medium';66 if (c > 0) return 'Low';67 return 'Insufficient';68}6970export function scoreLabel(s: number | null | undefined): string {71 if (s === null || s === undefined) return 'n/a';72 if (s >= 80) return 'Very high';73 if (s >= 60) return 'High';74 if (s >= 40) return 'Moderate';75 if (s >= 20) return 'Low';76 return 'Very low';77}7879export function cn(...parts: Array<string | false | null | undefined>): string {80 return parts.filter(Boolean).join(' ');81}82