Files
projects-jenz/RaceTimer_2026/racetimer_react/src/format.ts
T

75 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const number = new Intl.NumberFormat("en-US");
/** 12.345 -> "12.345", 83.2 -> "1:23.200", 3723.5 -> "1:02:03.500". */
export function formatTime(seconds: number): string {
const ms = Math.round(seconds * 1000);
const h = Math.floor(ms / 3_600_000);
const m = Math.floor((ms % 3_600_000) / 60_000);
const s = Math.floor((ms % 60_000) / 1000);
const rest = String(ms % 1000).padStart(3, "0");
if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${rest}`;
if (m > 0) return `${m}:${String(s).padStart(2, "0")}.${rest}`;
return `${s}.${rest}`;
}
/** Signed difference, e.g. "+1.204" or "−0.500". */
export function formatDelta(seconds: number): string {
const sign = seconds < 0 ? "−" : "+";
return sign + formatTime(Math.abs(seconds));
}
export function formatNumber(n: number): string {
return number.format(n);
}
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
["year", 31_536_000],
["month", 2_592_000],
["week", 604_800],
["day", 86_400],
["hour", 3_600],
["minute", 60],
];
/** Unix seconds -> "3 weeks ago". */
export function timeAgo(unixSeconds: number, now = Date.now() / 1000): string {
// Never show "in 3 minutes" when a server clock is slightly ahead.
const diff = Math.min(0, unixSeconds - now);
for (const [unit, size] of UNITS) {
if (Math.abs(diff) >= size) return rtf.format(Math.round(diff / size), unit);
}
return "just now";
}
/** Unix seconds -> "27 Sep 2026, 14:03:09" in the viewer's own time zone. */
export function formatDate(unixSeconds: number): string {
return new Date(unixSeconds * 1000).toLocaleString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
}
export function serverLabel(tag: string): string {
return tag.toUpperCase();
}
/** Which bonus band a position is in, for the label next to the multiplier. */
export function bonusLabel(position: number, completions: number): string | null {
if (completions < 100) return null;
const x = (position - 1) / completions;
if (x < 0.01) return "Top 1%";
if (x < 0.05) return "Top 5%";
return null;
}
/** How many players on a board get points (half from 200 completions on). */
export function pointedCount(completions: number): number {
return completions < 200 ? completions : Math.floor(completions / 2);
}