1const MINUTE = 60_000;
2const HOUR = 60 * MINUTE;
3const DAY = 24 * HOUR;
4
5export function formatScheduledTaskInterval(everyMs: number): string {
6 const units = [
7 [7 * DAY, "week"],
8 [DAY, "day"],
9 [HOUR, "hour"],
10 [MINUTE, "minute"],
11 [1_000, "second"],
12 [1, "millisecond"],
13 ] as const;
14 let remaining = everyMs;
15 const parts: string[] = [];
16 for (const [size, unit] of units) {
17 const count = Math.floor(remaining / size);
18 if (count === 0) continue;
19 if (count === 1 && remaining === everyMs && remaining === size) return `Every ${unit}`;
20 parts.push(`${count} ${unit}${count === 1 ? "" : "s"}`);
21 remaining %= size;
22 }
23 return `Every ${parts.join(" ")}`;
24}
25
26function localCalendarDay(date: Date): number {
27 // Compare calendar days, not 24-hour spans: DST days can have 23 or 25 hours.
28 return Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY;
29}
30
31export function formatNextScheduledTaskRun(nextRunAt: string, now: number): string {
32 const next = new Date(nextRunAt);
33 const current = new Date(now);
34 const remaining = next.getTime() - now;
35 if (!Number.isFinite(remaining)) return "Next run unavailable";
36 if (remaining <= 0) return "Next run due";
37
38 const days = localCalendarDay(next) - localCalendarDay(current);
39 if (days === 0) {
40 if (remaining < MINUTE) return "Next run in less than a minute";
41 const unit = remaining < HOUR ? "minute" : "hour";
42 const count = Math.round(remaining / (remaining < HOUR ? MINUTE : HOUR));
43 return `Next run in ${count} ${unit}${count === 1 ? "" : "s"}`;
44 }
45
46 const time = next.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
47 if (days === 1) return `Next run tomorrow at ${time}`;
48 if (days <= 7) {
49 return `Next run next ${next.toLocaleDateString([], { weekday: "long" })} at ${time}`;
50 }
51 const date = next.toLocaleDateString([], {
52 month: "short",
53 day: "numeric",
54 ...(next.getFullYear() !== current.getFullYear() ? { year: "numeric" as const } : {}),
55 });
56 return `Next run ${date} at ${time}`;
57}