SPB Git

Repository Public

format.ts 4.1 KB 85 lines typescript

1/** Formatting helpers. Null/undefined always renders as an em dash: the UI never invents values. */2export const DASH = '—';34export function fmtBytes(n: number | null | undefined): string {5  if (n == null || !Number.isFinite(n)) return DASH;6  if (n < 1024) return `${n} B`;7  const units = ['KiB', 'MiB', 'GiB', 'TiB'];8  let v = n / 1024;9  let i = 0;10  while (v >= 1024 && i < units.length - 1) {11    v /= 1024;12    i++;13  }14  return `${v >= 100 ? v.toFixed(0) : v.toFixed(1)} ${units[i]}`;15}16export function fmtNum(n: number | null | undefined, digits = 0, unit = ''): string {17  if (n == null || !Number.isFinite(n)) return DASH;18  return `${n.toLocaleString('en-CA', { minimumFractionDigits: digits, maximumFractionDigits: digits })}${unit}`;19}20export function fmtPct(n: number | null | undefined, digits = 0): string {21  return n == null || !Number.isFinite(n) ? DASH : `${n.toFixed(digits)} %`;22}23export function fmtTime(iso: string | number | null | undefined): string {24  if (iso == null) return DASH;25  const d = new Date(iso);26  return Number.isNaN(d.getTime()) ? DASH : d.toLocaleTimeString('en-CA', { hour12: false });27}28export function fmtDateTime(iso: string | number | null | undefined): string {29  if (iso == null) return DASH;30  const d = new Date(iso);31  if (Number.isNaN(d.getTime())) return DASH;32  return `${d.toLocaleDateString('en-CA')} ${d.toLocaleTimeString('en-CA', { hour12: false })}`;33}34export function fmtRelative(iso: string | number | null | undefined, nowMs = Date.now()): string {35  if (iso == null) return DASH;36  const t = new Date(iso).getTime();37  if (Number.isNaN(t)) return DASH;38  const s = Math.round((nowMs - t) / 1000);39  if (s < 5) return 'just now';40  if (s < 60) return `${s} s ago`;41  if (s < 3600) return `${Math.floor(s / 60)} min ago`;42  if (s < 86400) return `${Math.floor(s / 3600)} h ago`;43  if (s < 86400 * 30) return `${Math.floor(s / 86400)} d ago`;44  return new Date(t).toLocaleDateString('en-CA');45}46export function fmtDuration(sec: number | null | undefined): string {47  if (sec == null || !Number.isFinite(sec) || sec < 0) return DASH;48  const s = Math.floor(sec);49  if (s < 60) return `${s} s`;50  if (s < 3600) return `${Math.floor(s / 60)} min ${String(s % 60).padStart(2, '0')} s`;51  if (s < 86400) return `${Math.floor(s / 3600)} h ${String(Math.floor((s % 3600) / 60)).padStart(2, '0')} min`;52  return `${Math.floor(s / 86400)} d ${Math.floor((s % 86400) / 3600)} h`;53}54/** US dollars. Small amounts keep 3–4 decimals so that a cheap turn never reads "$0.00". */55export function fmtUsd(n: number | null | undefined, opts: { compact?: boolean } = {}): string {56  if (n == null || !Number.isFinite(n)) return DASH;57  const a = Math.abs(n);58  const digits = opts.compact ? (a >= 100 ? 0 : 2) : a === 0 || a >= 1 ? 2 : a >= 0.01 ? 3 : 4;59  return `$${n.toLocaleString('en-CA', { minimumFractionDigits: digits, maximumFractionDigits: digits })}`;60}61/** Token counts: 1 234 → "1.2k", 2 500 000 → "2.50M". */62export function fmtTokens(n: number | null | undefined): string {63  if (n == null || !Number.isFinite(n)) return DASH;64  if (n < 1000) return String(Math.round(n));65  if (n < 1e6) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;66  return `${(n / 1e6).toFixed(n < 1e7 ? 2 : 1)}M`;67}68export function fmtMs(ms: number | null | undefined): string {69  if (ms == null || !Number.isFinite(ms) || ms < 0) return DASH;70  if (ms < 1000) return `${Math.round(ms)} ms`;71  if (ms < 60_000) return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)} s`;72  return fmtDuration(ms / 1000);73}74/** Seconds between two instants (`end` null ⇒ now); null when the start is unknown. */75export function elapsedSec(start: string | null | undefined, end: string | null | undefined, nowMs = Date.now()): number | null {76  if (!start) return null;77  const a = new Date(start).getTime();78  const b = end ? new Date(end).getTime() : nowMs;79  return Number.isNaN(a) || Number.isNaN(b) ? null : Math.max(0, (b - a) / 1000);80}81export function truncate(s: string, max: number): string {82  return s.length <= max ? s : `${s.slice(0, max - 1)}…`;83}84export const isMac = (): boolean => typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);85
Files main
Menu

Theme