import { formatCompact, formatSC } from "@spinza/shared"; import { ApiClientError } from "@/lib/api"; /** Coerce Postgres bigint strings / nulls to a plain number. */ export function num(v: unknown): number { if (typeof v === "number") return Number.isFinite(v) ? v : 0; if (typeof v === "string" && v.trim() !== "") { const n = Number(v); return Number.isFinite(n) ? n : 0; } if (typeof v === "bigint") return Number(v); return 0; } export function pct(v: number | null | undefined, digits = 2): string { if (v === null || v === undefined || !Number.isFinite(v)) return "—"; return `${(v * 100).toFixed(digits)}%`; } export function signedPct(v: number | null | undefined, digits = 2): string { if (v === null || v === undefined || !Number.isFinite(v)) return "—"; return `${v > 0 ? "+" : ""}${(v * 100).toFixed(digits)}%`; } export function int(v: unknown): string { return Math.trunc(num(v)).toLocaleString("en-US"); } export function compact(v: unknown): string { return formatCompact(num(v)); } /** Signed credits: +1,250 SC / −400 SC. Never a currency sign. */ export function signedSC(v: unknown): string { const n = num(v); const s = formatSC(Math.abs(n)); return n < 0 ? `−${s}` : n > 0 ? `+${s}` : s; } export function sc(v: unknown): string { return formatSC(num(v)); } export function ms(v: number | null | undefined, digits = 0): string { if (v === null || v === undefined || !Number.isFinite(v)) return "—"; return `${v.toFixed(digits)} ms`; } /** 93784 -> "1d 2h 3m" */ export function duration(sec: number | null | undefined): string { if (sec === null || sec === undefined || !Number.isFinite(sec)) return "—"; const s = Math.max(0, Math.round(sec)); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); if (d > 0) return `${d}d ${h}h ${m}m`; if (h > 0) return `${h}h ${m}m`; if (m > 0) return `${m}m ${s % 60}s`; return `${s}s`; } export function durationMs(v: number | null | undefined): string { if (v === null || v === undefined || !Number.isFinite(v)) return "—"; if (v < 1000) return `${Math.round(v)} ms`; return duration(v / 1000); } export function bytes(v: unknown): string { const n = num(v); if (n < 1024) return `${n} B`; if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`; if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(1)} MB`; return `${(n / 1024 ** 3).toFixed(2)} GB`; } export function dateTime(iso: string | null | undefined): string { if (!iso) return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false }); } export function shortDate(iso: string | null | undefined): string { if (!iso) return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } export function isoDay(iso: string): string { const d = new Date(iso); return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10); } export function multiplier(v: unknown): string { const n = num(v); if (n >= 100) return `${Math.round(n).toLocaleString("en-US")}×`; if (n >= 10) return `${n.toFixed(1).replace(/\.0$/, "")}×`; return `${n.toFixed(2).replace(/\.?0+$/, "")}×`; } export function describeError(e: unknown): string { if (e instanceof ApiClientError) return e.code && e.code !== `HTTP_${e.status}` ? `${e.message} (${e.code})` : e.message; if (e instanceof Error) return e.message; return "Unexpected error."; } export function titleCase(s: string): string { return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); }