SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.6 KB · 82 lines typescript
Raw Blame History
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