110 lines
3.8 KiB
TypeScript

/** Formatting helpers for numbers, bytes, durations and timestamps. */
export function formatInt(value: number | null | undefined): string {
if (value === null || value === undefined || Number.isNaN(value)) return "—";
return value.toLocaleString("en-US");
}
export function formatBytes(value: number | null | undefined): string {
if (value === null || value === undefined || Number.isNaN(value)) return "—";
if (value < 1024) return `${value} B`;
const units = ["KiB", "MiB", "GiB", "TiB"];
let size = value;
let unit = "B";
for (const next of units) {
if (size < 1024) break;
size /= 1024;
unit = next;
}
return `${size >= 100 ? size.toFixed(0) : size.toFixed(1)} ${unit}`;
}
export function formatDurationMs(value: number | null | undefined): string {
if (value === null || value === undefined || Number.isNaN(value)) return "—";
if (value < 1000) return `${Math.round(value)} ms`;
const totalSeconds = Math.floor(value / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
const fraction = value / 1000;
return `${fraction >= 10 ? fraction.toFixed(0) : fraction.toFixed(1)}s`;
}
/** Parse an ISO/RFC3339 timestamp; returns null when unparseable. */
export function parseTime(value: string | null | undefined): Date | null {
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
/** "2026-07-17 01:46:55 UTC" — stable, timezone-explicit rendering. */
export function formatUtc(value: string | null | undefined): string {
const date = parseTime(value);
if (!date) return value ? String(value) : "—";
const pad = (n: number) => String(n).padStart(2, "0");
return (
`${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ` +
`${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC`
);
}
export function formatRelative(value: string | null | undefined): string {
const date = parseTime(value);
if (!date) return "—";
const deltaMs = Date.now() - date.getTime();
const future = deltaMs < 0;
const abs = Math.abs(deltaMs);
const minutes = Math.floor(abs / 60_000);
const hours = Math.floor(abs / 3_600_000);
const days = Math.floor(abs / 86_400_000);
let text: string;
if (abs < 60_000) text = "just now";
else if (minutes < 60) text = `${minutes}m`;
else if (hours < 24) text = `${hours}h`;
else text = `${days}d`;
if (text === "just now") return text;
return future ? `in ${text}` : `${text} ago`;
}
export function formatPercent(part: number, total: number): string {
if (!total) return "—";
return `${((part / total) * 100).toFixed(1)}%`;
}
/** Truncate a long identifier for display, keeping head and tail. */
export function truncateMiddle(value: string, max = 32): string {
if (value.length <= max) return value;
const head = Math.ceil((max - 1) / 2);
const tail = Math.floor((max - 1) / 2);
return `${value.slice(0, head)}${value.slice(value.length - tail)}`;
}
/** Human label for an object type code (accepts both report-stream and projection vocabularies). */
export function objectTypeLabel(type: string | null | undefined): string {
switch ((type ?? "").toLowerCase()) {
case "roa":
return "ROA";
case "mft":
case "manifest":
return "Manifest";
case "crl":
return "CRL";
case "cer":
case "certificate":
return "Certificate";
case "asa":
case "aspa":
return "ASPA";
case "gbr":
return "Ghostbusters";
case "ee":
return "EE Certificate";
case "other":
return "Other";
default:
return type ? type.toUpperCase() : "—";
}
}