SPB Git

spb/findata

Public
5.5 KB · 149 lines typescript
1/** Formatting helpers — bytes, thin-space grouped numbers, ISO UTC timestamps. */23const THIN = " "; // narrow no-break space45export function fmtInt(n: number | null | undefined): string {6  if (n === null || n === undefined || Number.isNaN(Number(n))) return "—";7  const v = Math.round(Number(n));8  const neg = v < 0;9  const s = Math.abs(v).toString();10  const parts: string[] = [];11  for (let i = s.length; i > 0; i -= 3) parts.unshift(s.slice(Math.max(0, i - 3), i));12  return (neg ? "-" : "") + parts.join(THIN);13}1415export function fmtNum(n: number | null | undefined, digits = 2): string {16  if (n === null || n === undefined || Number.isNaN(Number(n))) return "—";17  const v = Number(n);18  if (Number.isInteger(v) && digits === 0) return fmtInt(v);19  const [i, f] = v.toFixed(digits).split(".");20  return fmtInt(Number(i)) + (f ? "." + f : "");21}2223/** Compact 1 284 / 12.9K / 4.2M / 1.3B */24export function fmtCompact(n: number | null | undefined): string {25  if (n === null || n === undefined || Number.isNaN(Number(n))) return "—";26  const v = Number(n);27  const a = Math.abs(v);28  if (a < 10_000) return fmtInt(v);29  if (a < 1e6) return (v / 1e3).toFixed(1).replace(/\.0$/, "") + "K";30  if (a < 1e9) return (v / 1e6).toFixed(1).replace(/\.0$/, "") + "M";31  if (a < 1e12) return (v / 1e9).toFixed(2).replace(/\.?0+$/, "") + "B";32  return (v / 1e12).toFixed(2).replace(/\.?0+$/, "") + "T";33}3435export function fmtBytes(b: number | null | undefined, digits = 1): string {36  if (b === null || b === undefined || Number.isNaN(Number(b))) return "—";37  const v = Number(b);38  if (v < 1e3) return `${fmtInt(v)} B`;39  if (v < 1e6) return `${(v / 1e3).toFixed(digits)} kB`;40  if (v < 1e9) return `${(v / 1e6).toFixed(digits)} MB`;41  if (v < 1e12) return `${(v / 1e9).toFixed(digits)} GB`;42  return `${(v / 1e12).toFixed(digits)} TB`;43}4445export function fmtGB(gb: number | null | undefined, digits = 1): string {46  if (gb === null || gb === undefined) return "—";47  return `${Number(gb).toFixed(digits)} GB`;48}4950export function fmtPct(x: number | null | undefined, digits = 1): string {51  if (x === null || x === undefined || Number.isNaN(Number(x))) return "—";52  return `${(Number(x) * 100).toFixed(digits)} %`;53}5455const pad = (n: number) => String(n).padStart(2, "0");5657/** ISO UTC → `YYYY-MM-DD HH:mm:ssZ` */58export function fmtTs(iso: string | null | undefined): string {59  if (!iso) return "—";60  const d = new Date(iso);61  if (Number.isNaN(d.getTime())) return String(iso);62  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;63}6465/** Short time-of-day `HH:mm` (UTC) */66export function fmtTime(iso: string | null | undefined): string {67  if (!iso) return "—";68  const d = new Date(iso);69  if (Number.isNaN(d.getTime())) return String(iso);70  return `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;71}7273export function fmtDate(iso: string | null | undefined): string {74  if (!iso) return "—";75  return String(iso).slice(0, 10);76}7778export function fmtRelative(iso: string | null | undefined): string {79  if (!iso) return "—";80  const d = new Date(iso).getTime();81  if (Number.isNaN(d)) return String(iso);82  const s = Math.round((Date.now() - d) / 1000);83  const abs = Math.abs(s);84  const suffix = s >= 0 ? "ago" : "from now";85  if (abs < 60) return `${abs} s ${suffix}`;86  if (abs < 3600) return `${Math.round(abs / 60)} min ${suffix}`;87  if (abs < 86400) return `${(abs / 3600).toFixed(1)} h ${suffix}`;88  return `${Math.round(abs / 86400)} d ${suffix}`;89}9091export function fmtDuration(startIso: string | null | undefined, endIso: string | null | undefined): string {92  if (!startIso) return "—";93  const a = new Date(startIso).getTime();94  const b = endIso ? new Date(endIso).getTime() : Date.now();95  const s = Math.max(0, Math.round((b - a) / 1000));96  if (s < 60) return `${s} s`;97  if (s < 3600) return `${Math.floor(s / 60)} min ${s % 60} s`;98  return `${(s / 3600).toFixed(1)} h`;99}100101export function isoDate(d: Date): string {102  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;103}104105export function daysAgo(n: number): string {106  const d = new Date();107  d.setUTCDate(d.getUTCDate() - n);108  return isoDate(d);109}110111export function today(): string {112  return isoDate(new Date());113}114115/** datetime-local value (local tz) for `now` */116export function nowLocalInput(): string {117  const d = new Date();118  const off = d.getTimezoneOffset();119  const local = new Date(d.getTime() - off * 60_000);120  return local.toISOString().slice(0, 16);121}122123export function toCsv(columns: string[], rows: Record<string, unknown>[]): string {124  const esc = (v: unknown) => {125    if (v === null || v === undefined) return "";126    const s = typeof v === "object" ? JSON.stringify(v) : String(v);127    return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;128  };129  const lines = [columns.map(esc).join(",")];130  for (const r of rows) lines.push(columns.map((c) => esc(r[c])).join(","));131  return lines.join("\n");132}133134export function downloadText(filename: string, text: string, mime = "text/csv;charset=utf-8") {135  const blob = new Blob([text], { type: mime });136  const url = URL.createObjectURL(blob);137  const a = document.createElement("a");138  a.href = url;139  a.download = filename;140  document.body.appendChild(a);141  a.click();142  a.remove();143  setTimeout(() => URL.revokeObjectURL(url), 1000);144}145146export function splitList(text: string): string[] {147  return Array.from(new Set(text.split(/[\s,;]+/).map((s) => s.trim().toUpperCase()).filter(Boolean)));148}149
Files · de3e9e393936b8d9c1340b2c85d7ccb9ffa8264a
Menu

Theme