const DIVISIONS: { amount: number; unit: Intl.RelativeTimeFormatUnit }[] = [ { amount: 60, unit: "seconds" }, { amount: 60, unit: "minutes" }, { amount: 24, unit: "hours" }, { amount: 7, unit: "days" }, { amount: 4.34524, unit: "weeks" }, { amount: 12, unit: "months" }, { amount: Number.POSITIVE_INFINITY, unit: "years" } ]; const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); const dtf = new Intl.DateTimeFormat("en", { year: "numeric", month: "short", day: "numeric" }); // the appview's longTimeFmt ("Jan 2, 2006, 3:04 PM MST") const dtfFull = new Intl.DateTimeFormat("en", { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }); // "3 days ago", "in 2 hours", etc. export const relativeTime = (input: string | Date, now: Date = new Date()): string => { const date = typeof input === "string" ? new Date(input) : input; if (Number.isNaN(date.getTime())) return ""; let duration = (date.getTime() - now.getTime()) / 1000; for (const division of DIVISIONS) { if (Math.abs(duration) < division.amount) return rtf.format(Math.round(duration), division.unit); duration /= division.amount; } return rtf.format(Math.round(duration), "years"); }; export const compactRelativeTime = (input: string | Date, now: Date = new Date()): string => { const date = typeof input === "string" ? new Date(input) : input; if (Number.isNaN(date.getTime())) return ""; let duration = Math.abs((now.getTime() - date.getTime()) / 1000); const suffix = date.getTime() > now.getTime() ? "from now" : "ago"; if (duration < 60) return `${Math.floor(duration)}s ${suffix}`; duration /= 60; if (duration < 60) return `${Math.floor(duration)}m ${suffix}`; duration /= 60; if (duration < 24) return `${Math.floor(duration)}h ${suffix}`; duration /= 24; if (duration < 7) return `${Math.floor(duration)}d ${suffix}`; duration /= 7; if (duration < 4.34524) return `${Math.floor(duration)}w ${suffix}`; duration /= 4.34524; if (duration < 12) return `${Math.floor(duration)}mo ${suffix}`; duration /= 12; return `${Math.floor(duration)}y ${suffix}`; }; // the appview's durationFmt: non-zero chunks, largest first ("1h 3m 20s"). // under a second reads "0s", so an instant run still differs from one that never ran. export const formatDuration = (ms: number): string => { if (!Number.isFinite(ms) || ms < 0) return ""; const total = Math.floor(ms / 1000); const chunks: [number, string][] = [ [Math.floor(total / 86400), "d"], [Math.floor(total / 3600) % 24, "h"], [Math.floor(total / 60) % 60, "m"], [total % 60, "s"] ]; const parts = chunks .filter(([amount]) => amount !== 0) .map(([amount, unit]) => `${amount}${unit}`); return parts.length > 0 ? parts.join(" ") : "0s"; }; export const formatDate = (input: string | Date): string => { const date = typeof input === "string" ? new Date(input) : input; return Number.isNaN(date.getTime()) ? "" : dtf.format(date); }; export const formatDateTime = (input: string | Date): string => { const date = typeof input === "string" ? new Date(input) : input; return Number.isNaN(date.getTime()) ? "" : dtfFull.format(date); };