const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }); const usdPrecise = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2, }); const number = new Intl.NumberFormat("en-US"); const date = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", }); const dateShort = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", }); const time = new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", }); const relative = new Intl.RelativeTimeFormat("en-US", { numeric: "auto" }); /** * Coerce a backend Decimal-serialized value (string) or a number to a finite * number. Returns the supplied fallback (default 0) on null/undefined/NaN. * Used everywhere the FastAPI `Decimal` field needs to flow into a UI that * expects `number`. */ export const toNum = (v: string | number | null | undefined, fallback = 0): number => { if (v === null || v === undefined) return fallback; const n = typeof v === "number" ? v : Number(v); return Number.isFinite(n) ? n : fallback; }; export const fmt = { usd: (n: number) => usd.format(n), usdPrecise: (n: number) => usdPrecise.format(n), usdDecimal: (v: string | number | null | undefined) => usdPrecise.format(toNum(v)), num: (n: number) => number.format(n), date: (iso: string) => date.format(new Date(iso)), dateShort: (iso: string) => dateShort.format(new Date(iso)), time: (iso: string) => time.format(new Date(iso)), pct: (n: number, digits = 1) => `${n.toFixed(digits)}%`, relative: (iso: string) => { const ms = new Date(iso).getTime() - Date.now(); const minutes = Math.round(ms / 60_000); if (Math.abs(minutes) < 60) return relative.format(minutes, "minute"); const hours = Math.round(minutes / 60); if (Math.abs(hours) < 24) return relative.format(hours, "hour"); const days = Math.round(hours / 24); if (Math.abs(days) < 7) return relative.format(days, "day"); return fmt.date(iso); }, };