/** * Every number the reader sees, formatted in one place (milestone 39). * * The rules are fixed so that two surfaces never spell one quantity two ways: * counts are grouped, percentages always carry two decimals, rates one, and a * duration is its two largest nonzero units with no zero padding. A call site * that formats inline is a defect; the sweep test in `format.test.ts` and the * review both hunt for one. * * What the contract covers is a measured quantity the reader compares: a count, * a share, a rate, a duration, a size in bytes, a time. Three kinds of number * are not quantities and render as written. An identifier or a protocol code — * a group or source id, an RCODE, a QCLASS, an unknown QTYPE's number — names a * thing rather than measuring one, though a row's ordinal is a count. A * configured value shown next to or inside the input that edits it — a TTL of * 3600, a cache size of 10000, a priority — must read back exactly as the * operator typed it. And the fixed label of a preset in a menu — "Past 24 * hours", "5 minutes" — is copy, not a measurement. */ /** Unix seconds → localized date-time. `locale`/`timeZone` exist for deterministic tests. */ export function formatTime(unixSeconds: number, locale?: string, timeZone?: string): string { return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "medium", timeZone, }).format(new Date(unixSeconds * 1000)); } /** * Unix seconds → the time of day alone, "14:05". For a stamp the reader places * against now — a pause that ends shortly, the last row that was dropped — * where the date would be noise on every reading but one. */ export function formatClock(unixSeconds: number, locale?: string, timeZone?: string): string { return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone }).format( new Date(unixSeconds * 1000), ); } /** * The label on a chart's time axis: the day for buckets a day or wider, the * time of day otherwise. */ export function formatBucketTime(unixSeconds: number, bucketSeconds: number): string { const date = new Date(unixSeconds * 1000); if (bucketSeconds >= 86_400) { return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" }).format(date); } return new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" }).format(date); } const grouped = new Intl.NumberFormat("en-US"); /** A count, thousands-grouped: 18432 → "18,432". */ export function formatCount(count: number): string { return grouped.format(count); } /** * Round half up at `decimals` places. `toFixed` rounds the binary value, so * 15.435 (stored just under) would print 15.43; the nudge lifts an exact half * over the edge without moving anything else. */ function roundHalfUp(value: number, decimals: number): string { const scale = 10 ** decimals; return (Math.round(value * scale + 1e-9) / scale).toFixed(decimals); } /** * A share as a percentage, always two decimals: 0.1544 → "15.44%", 0 → * "0.00%", 1 → "100.00%". The caller decides what a share of nothing means; a * total of zero is not this function's to guess. */ export function formatPercent(fraction: number): string { return `${roundHalfUp(fraction * 100, 2)}%`; } /** A rate, one decimal: 12.84 → "12.8", 5 → "5.0". */ export function formatRate(value: number): string { return roundHalfUp(value, 1); } const BYTE_UNITS = ["KiB", "MiB", "GiB", "TiB"] as const; export function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; let value = bytes; let unit: string = BYTE_UNITS[0]; for (const next of BYTE_UNITS) { unit = next; value /= 1024; if (value < 1024) break; } return `${roundHalfUp(value, 1)} ${unit}`; } const DURATION_UNITS = [ { seconds: 86400, suffix: "d" }, { seconds: 3600, suffix: "h" }, { seconds: 60, suffix: "m" }, { seconds: 1, suffix: "s" }, ] as const; /** * Seconds of elapsed time → its two largest nonzero units, unpadded: "6d 4h", * "4h 12m", "12m 5s", "6d 5s", "45s". Two units, because "6d" alone hides * four hours and "6d 4h 12m 5s" is a stopwatch; nonzero, because "6d 0h" says * nothing "6d 5s" does not. Anything under a second, a negative span included * — clock skew is not a duration — reads "<1s". */ export function formatDuration(seconds: number): string { let rest = Math.floor(seconds); if (rest < 1) return "<1s"; const parts: string[] = []; for (const unit of DURATION_UNITS) { const amount = Math.floor(rest / unit.seconds); rest -= amount * unit.seconds; if (amount > 0) parts.push(`${amount}${unit.suffix}`); } return parts.slice(0, 2).join(" "); } /** Microseconds → milliseconds with one decimal, e.g. 1234 → "1.2 ms". */ export function formatMicros(micros: number): string { return `${formatRate(micros / 1000)} ms`; }