185 lines
4.8 KiB
TypeScript
185 lines
4.8 KiB
TypeScript
const THOUSAND = 1000;
|
||
const MILLION = 1_000_000;
|
||
const MINUTE_MS = 60_000;
|
||
const HOUR_MS = 3_600_000;
|
||
const DAY_MS = 86_400_000;
|
||
const KB = 1024;
|
||
const WHITESPACE = /\s+/g;
|
||
|
||
export function fmtTokens(n: number | null | undefined): string {
|
||
if (n === null || n === undefined) {
|
||
return "–";
|
||
}
|
||
if (n >= MILLION) {
|
||
return `${(n / MILLION).toFixed(n >= 10 * MILLION ? 0 : 1)}M`;
|
||
}
|
||
if (n >= THOUSAND) {
|
||
return `${(n / THOUSAND).toFixed(n >= 10 * THOUSAND ? 0 : 1)}k`;
|
||
}
|
||
return String(n);
|
||
}
|
||
|
||
export function fmtMoney(usd: number | null | undefined): string {
|
||
if (usd === null || usd === undefined) {
|
||
return "–";
|
||
}
|
||
if (usd === 0) {
|
||
return "$0";
|
||
}
|
||
if (usd < 0.01) {
|
||
return `$${usd.toFixed(4)}`;
|
||
}
|
||
if (usd < 1) {
|
||
return `$${usd.toFixed(3)}`;
|
||
}
|
||
return `$${usd.toFixed(2)}`;
|
||
}
|
||
|
||
export function fmtPct(fraction: number | null | undefined): string {
|
||
if (fraction === null || fraction === undefined) {
|
||
return "–";
|
||
}
|
||
return `${Math.round(fraction * 100)}%`;
|
||
}
|
||
|
||
export function fmtDuration(ms: number | null | undefined): string {
|
||
if (ms === null || ms === undefined) {
|
||
return "–";
|
||
}
|
||
if (ms < THOUSAND) {
|
||
return `${Math.round(ms)}ms`;
|
||
}
|
||
if (ms < MINUTE_MS) {
|
||
return `${(ms / THOUSAND).toFixed(1)}s`;
|
||
}
|
||
if (ms < HOUR_MS) {
|
||
const minutes = Math.floor(ms / MINUTE_MS);
|
||
const seconds = Math.round((ms % MINUTE_MS) / THOUSAND);
|
||
return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
||
}
|
||
const hours = Math.floor(ms / HOUR_MS);
|
||
const minutes = Math.round((ms % HOUR_MS) / MINUTE_MS);
|
||
return `${hours}h${String(minutes).padStart(2, "0")}m`;
|
||
}
|
||
|
||
export function fmtSeconds(seconds: number | null | undefined): string {
|
||
return seconds === null || seconds === undefined
|
||
? "–"
|
||
: fmtDuration(seconds * THOUSAND);
|
||
}
|
||
|
||
export function fmtBytes(bytes: number | null | undefined): string {
|
||
if (bytes === null || bytes === undefined) {
|
||
return "–";
|
||
}
|
||
if (bytes >= KB * KB * KB) {
|
||
return `${(bytes / (KB * KB * KB)).toFixed(2)} GB`;
|
||
}
|
||
if (bytes >= KB * KB) {
|
||
return `${(bytes / (KB * KB)).toFixed(0)} MB`;
|
||
}
|
||
if (bytes >= KB) {
|
||
return `${(bytes / KB).toFixed(0)} kB`;
|
||
}
|
||
return `${bytes} B`;
|
||
}
|
||
|
||
export function parseDate(iso: string | null | undefined): Date | null {
|
||
if (!iso) {
|
||
return null;
|
||
}
|
||
const date = new Date(iso);
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
}
|
||
|
||
export function fmtTime(iso: string | null | undefined): string {
|
||
const date = parseDate(iso);
|
||
return date
|
||
? date.toLocaleTimeString(undefined, {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
})
|
||
: "–";
|
||
}
|
||
|
||
export function fmtDateTime(iso: string | null | undefined): string {
|
||
const date = parseDate(iso);
|
||
return date
|
||
? date.toLocaleString(undefined, {
|
||
day: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
month: "short",
|
||
})
|
||
: "–";
|
||
}
|
||
|
||
export function fmtRelative(
|
||
iso: string | null | undefined,
|
||
now = Date.now()
|
||
): string {
|
||
const date = parseDate(iso);
|
||
if (!date) {
|
||
return "–";
|
||
}
|
||
const diff = now - date.getTime();
|
||
const abs = Math.abs(diff);
|
||
const suffix = diff >= 0 ? "ago" : "from now";
|
||
if (abs < MINUTE_MS) {
|
||
return diff >= 0 ? "just now" : "in <1m";
|
||
}
|
||
if (abs < HOUR_MS) {
|
||
return `${Math.round(abs / MINUTE_MS)}m ${suffix}`;
|
||
}
|
||
if (abs < DAY_MS) {
|
||
return `${Math.round(abs / HOUR_MS)}h ${suffix}`;
|
||
}
|
||
return `${Math.round(abs / DAY_MS)}d ${suffix}`;
|
||
}
|
||
|
||
export function fmtCountdown(iso: string | null | undefined, now = Date.now()) {
|
||
const date = parseDate(iso);
|
||
if (!date) {
|
||
return "–";
|
||
}
|
||
const left = date.getTime() - now;
|
||
if (left <= 0) {
|
||
return "resets now";
|
||
}
|
||
if (left < HOUR_MS) {
|
||
return `resets in ${Math.ceil(left / MINUTE_MS)}m`;
|
||
}
|
||
if (left < DAY_MS) {
|
||
const hours = Math.floor(left / HOUR_MS);
|
||
const minutes = Math.round((left % HOUR_MS) / MINUTE_MS);
|
||
return `resets in ${hours}h ${String(minutes).padStart(2, "0")}m`;
|
||
}
|
||
const days = Math.floor(left / DAY_MS);
|
||
const hours = Math.round((left % DAY_MS) / HOUR_MS);
|
||
return `resets in ${days}d ${hours}h`;
|
||
}
|
||
|
||
export function elapsedMs(from: string, to: string | null, now = Date.now()) {
|
||
const a = parseDate(from)?.getTime();
|
||
const b = to ? parseDate(to)?.getTime() : now;
|
||
if (a === undefined || b === undefined) {
|
||
return 0;
|
||
}
|
||
return Math.max(0, b - a);
|
||
}
|
||
|
||
export function clip(text: string, max: number): string {
|
||
const one = text.replace(WHITESPACE, " ").trim();
|
||
return one.length > max ? `${one.slice(0, max - 1)}…` : one;
|
||
}
|
||
|
||
export function shortId(id: string | null | undefined): string {
|
||
return id ? id.slice(0, 8) : "–";
|
||
}
|
||
|
||
export function cacheShare(input: number, cacheRead: number): number | null {
|
||
const total = input + cacheRead;
|
||
return total > 0 ? cacheRead / total : null;
|
||
}
|