import type { CurrencyCode } from '@rareindex/shared'; /** Display formatting shared by server and client components (no Node-only imports). */ export function fmtMoney(amount: number | null | undefined, currency: CurrencyCode | string = 'USD', opts: { compact?: boolean; digits?: number } = {}): string { if (amount === null || amount === undefined || !Number.isFinite(amount)) return '—'; const zero = currency === 'JPY' || currency === 'KRW'; const digits = opts.digits ?? (zero ? 0 : Math.abs(amount) >= 1000 ? 0 : 2); try { return new Intl.NumberFormat('en-US', { style: 'currency', currency, notation: opts.compact ? 'compact' : 'standard', maximumFractionDigits: digits, minimumFractionDigits: opts.compact ? 0 : digits }).format(amount); } catch { return `${amount.toFixed(digits)} ${currency}`; } } export function fmtPct(value: number | null | undefined, digits = 2, signed = true): string { if (value === null || value === undefined || !Number.isFinite(value)) return '—'; const sign = signed && value > 0 ? '+' : ''; return `${sign}${(value * 100).toFixed(digits)}%`; } export function fmtNum(value: number | null | undefined, opts: { compact?: boolean; digits?: number } = {}): string { if (value === null || value === undefined || !Number.isFinite(value)) return '—'; return new Intl.NumberFormat('en-US', { notation: opts.compact ? 'compact' : 'standard', maximumFractionDigits: opts.digits ?? (opts.compact ? 1 : 0) }).format(value); } export function fmtDate(d: Date | string | null | undefined, opts: { month?: 'short' | 'long' | 'numeric'; year?: boolean; time?: boolean } = {}): string { if (!d) return '—'; const date = typeof d === 'string' ? new Date(d.length === 10 ? `${d}T00:00:00Z` : d) : d; if (Number.isNaN(date.getTime())) return '—'; 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); } export function fmtRelative(d: Date | string | null | undefined): string { if (!d) return '—'; const date = typeof d === 'string' ? new Date(d) : d; const diff = Date.now() - date.getTime(); const abs = Math.abs(diff); const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); const units: Array<[number, Intl.RelativeTimeFormatUnit]> = [ [60_000, 'second'], [3_600_000, 'minute'], [86_400_000, 'hour'], [604_800_000, 'day'], [2_629_800_000, 'week'], [31_557_600_000, 'month'], [Infinity, 'year'], ]; let prevMs = 1; for (const [ms, unit] of units) { if (abs < ms) return rtf.format(Math.round(-diff / prevMs), unit); prevMs = ms; } return fmtDate(date); } export function deltaClass(value: number | null | undefined): string { if (value === null || value === undefined || !Number.isFinite(value) || Math.abs(value) < 0.00005) return 'text-flat'; return value > 0 ? 'text-gain' : 'text-loss'; } export function confidenceLabel(c: number | null | undefined): 'High' | 'Medium' | 'Low' | 'Insufficient' { if (c === null || c === undefined) return 'Insufficient'; if (c >= 0.75) return 'High'; if (c >= 0.5) return 'Medium'; if (c > 0) return 'Low'; return 'Insufficient'; } export function scoreLabel(s: number | null | undefined): string { if (s === null || s === undefined) return 'n/a'; if (s >= 80) return 'Very high'; if (s >= 60) return 'High'; if (s >= 40) return 'Moderate'; if (s >= 20) return 'Low'; return 'Very low'; } export function cn(...parts: Array): string { return parts.filter(Boolean).join(' '); }