SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
3.7 KB · 111 lines typescript
Raw Blame History
1import { formatCompact, formatSC } from "@spinza/shared";2import { ApiClientError } from "@/lib/api";34/** Coerce Postgres bigint strings / nulls to a plain number. */5export function num(v: unknown): number {6  if (typeof v === "number") return Number.isFinite(v) ? v : 0;7  if (typeof v === "string" && v.trim() !== "") {8    const n = Number(v);9    return Number.isFinite(n) ? n : 0;10  }11  if (typeof v === "bigint") return Number(v);12  return 0;13}1415export function pct(v: number | null | undefined, digits = 2): string {16  if (v === null || v === undefined || !Number.isFinite(v)) return "—";17  return `${(v * 100).toFixed(digits)}%`;18}1920export function signedPct(v: number | null | undefined, digits = 2): string {21  if (v === null || v === undefined || !Number.isFinite(v)) return "—";22  return `${v > 0 ? "+" : ""}${(v * 100).toFixed(digits)}%`;23}2425export function int(v: unknown): string {26  return Math.trunc(num(v)).toLocaleString("en-US");27}2829export function compact(v: unknown): string {30  return formatCompact(num(v));31}3233/** Signed credits: +1,250 SC / −400 SC. Never a currency sign. */34export function signedSC(v: unknown): string {35  const n = num(v);36  const s = formatSC(Math.abs(n));37  return n < 0 ? `−${s}` : n > 0 ? `+${s}` : s;38}3940export function sc(v: unknown): string {41  return formatSC(num(v));42}4344export function ms(v: number | null | undefined, digits = 0): string {45  if (v === null || v === undefined || !Number.isFinite(v)) return "—";46  return `${v.toFixed(digits)} ms`;47}4849/** 93784 -> "1d 2h 3m" */50export function duration(sec: number | null | undefined): string {51  if (sec === null || sec === undefined || !Number.isFinite(sec)) return "—";52  const s = Math.max(0, Math.round(sec));53  const d = Math.floor(s / 86400);54  const h = Math.floor((s % 86400) / 3600);55  const m = Math.floor((s % 3600) / 60);56  if (d > 0) return `${d}d ${h}h ${m}m`;57  if (h > 0) return `${h}h ${m}m`;58  if (m > 0) return `${m}m ${s % 60}s`;59  return `${s}s`;60}6162export function durationMs(v: number | null | undefined): string {63  if (v === null || v === undefined || !Number.isFinite(v)) return "—";64  if (v < 1000) return `${Math.round(v)} ms`;65  return duration(v / 1000);66}6768export function bytes(v: unknown): string {69  const n = num(v);70  if (n < 1024) return `${n} B`;71  if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`;72  if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(1)} MB`;73  return `${(n / 1024 ** 3).toFixed(2)} GB`;74}7576export function dateTime(iso: string | null | undefined): string {77  if (!iso) return "—";78  const d = new Date(iso);79  if (Number.isNaN(d.getTime())) return "—";80  return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false });81}8283export function shortDate(iso: string | null | undefined): string {84  if (!iso) return "—";85  const d = new Date(iso);86  if (Number.isNaN(d.getTime())) return "—";87  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });88}8990export function isoDay(iso: string): string {91  const d = new Date(iso);92  return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10);93}9495export function multiplier(v: unknown): string {96  const n = num(v);97  if (n >= 100) return `${Math.round(n).toLocaleString("en-US")}×`;98  if (n >= 10) return `${n.toFixed(1).replace(/\.0$/, "")}×`;99  return `${n.toFixed(2).replace(/\.?0+$/, "")}×`;100}101102export function describeError(e: unknown): string {103  if (e instanceof ApiClientError) return e.code && e.code !== `HTTP_${e.status}` ? `${e.message} (${e.code})` : e.message;104  if (e instanceof Error) return e.message;105  return "Unexpected error.";106}107108export function titleCase(s: string): string {109  return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());110}111