Files
moltbot/src/infra/format-time/format-relative.ts

113 lines
3.3 KiB
TypeScript

/**
* 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`;
}
}