/** * Centralized relative-time formatting utilities. * * Consolidates 7+ scattered implementations (formatAge, formatAgeShort, formatAgo, * formatRelativeTime, formatElapsedTime) into two functions: * * - `formatTimeAgo(durationMs)` — format a duration as "5m ago" / "5m" (for known elapsed time) * - `formatRelativeTimestamp(epochMs)` — format an epoch timestamp relative to now (handles future) */ export type FormatTimeAgoOptions = { /** Append "ago" suffix. Default: true. When false, returns bare unit: "5m", "2h" */ suffix?: boolean; /** Return value for invalid/null/negative input. Default: "unknown" */ fallback?: string; }; /** * Format a duration (in ms) as a human-readable relative time. * * Input: how many milliseconds ago something happened. * * With suffix (default): "just now", "5m ago", "3h ago", "2d ago" * Without suffix: "0s", "5m", "3h", "2d" */ export function formatTimeAgo( durationMs: number | null | undefined, options?: FormatTimeAgoOptions, ): string { const suffix = options?.suffix !== false; const fallback = options?.fallback ?? "unknown"; if (durationMs == null || !Number.isFinite(durationMs) || durationMs < 0) { return fallback; } const totalSeconds = Math.round(durationMs / 1000); const minutes = Math.round(totalSeconds / 60); if (minutes < 1) { return suffix ? "just now" : `${totalSeconds}s`; } if (minutes < 60) { return suffix ? `${minutes}m ago` : `${minutes}m`; } const hours = Math.round(minutes / 60); if (hours < 48) { return suffix ? `${hours}h ago` : `${hours}h`; } const days = Math.round(hours / 24); return suffix ? `${days}d ago` : `${days}d`; } export type FormatRelativeTimestampOptions = { /** If true, fall back to short date (e.g. "Oct 5") for timestamps >7 days. Default: false */ dateFallback?: boolean; /** IANA timezone for date fallback display */ timezone?: string; /** Return value for invalid/null input. Default: "n/a" */ fallback?: string; }; /** * Format an epoch timestamp relative to now. * * Handles both past ("5m ago") and future ("in 5m") timestamps. * Optionally falls back to a short date for timestamps older than 7 days. */ export function formatRelativeTimestamp( timestampMs: number | null | undefined, options?: FormatRelativeTimestampOptions, ): string { const fallback = options?.fallback ?? "n/a"; if (timestampMs == null || !Number.isFinite(timestampMs)) { return fallback; } const diff = Date.now() - timestampMs; const absDiff = Math.abs(diff); const isPast = diff >= 0; const sec = Math.round(absDiff / 1000); if (sec < 60) { return isPast ? "just now" : "in <1m"; } const min = Math.round(sec / 60); if (min < 60) { return isPast ? `${min}m ago` : `in ${min}m`; } const hr = Math.round(min / 60); if (hr < 48) { return isPast ? `${hr}h ago` : `in ${hr}h`; } const day = Math.round(hr / 24); if (!options?.dateFallback || day <= 7) { return isPast ? `${day}d ago` : `in ${day}d`; } // Fall back to short date display for old timestamps try { return new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", ...(options.timezone ? { timeZone: options.timezone } : {}), }).format(new Date(timestampMs)); } catch { return `${day}d ago`; } }