1
8
9export const DAY_MS = 86_400_000;
10
11
12export function normalizeDate(input: unknown, withTime = false): string {
13 if (typeof input === "number")
14 return new Date(input).toISOString().slice(0, withTime ? undefined : 10);
15 if (typeof input === "string") {
16 const s = input.trim();
17 const dayOnly = /^\d{4}-\d{2}-\d{2}$/.test(s);
18 const full = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/.test(s);
19 if (!dayOnly && !full) throw new Error(`invalid date value: ${input}`);
20 const d = new Date(s.length === 10 ? s + "T00:00:00Z" : s);
21 if (Number.isNaN(d.getTime())) throw new Error(`invalid date value: ${input}`);
22 return dayOnly && !withTime ? s : d.toISOString().slice(0, withTime ? undefined : 10);
23 }
24 throw new Error(`invalid date value: ${String(input)}`);
25}
26
27
28export function dateToMs(value: string): number {
29 const d = new Date(value.length === 10 ? value + "T00:00:00Z" : value);
30 if (Number.isNaN(d.getTime())) throw new Error(`invalid date value: ${value}`);
31 return d.getTime();
32}
33
34
35export function utcDay(value: string): number {
36 return Math.floor(dateToMs(value) / DAY_MS);
37}
38
39
40export function addDays(value: string, days: number): string {
41 return new Date(utcDay(value) * DAY_MS + days * DAY_MS).toISOString().slice(0, 10);
42}
43
44
45export function daysBetween(a: string, b: string): number {
46 return utcDay(b) - utcDay(a);
47}
48
49
54export function addCalendarUnit(
55 value: string,
56 amount: number,
57 unit: "days" | "weeks" | "months" | "years",
58): string {
59 if (unit === "days") return addDays(value, amount);
60 if (unit === "weeks") return addDays(value, amount * 7);
61 const iso = value.length === 10 ? value : value.slice(0, 10);
62 const [y = 0, m = 1, d = 1] = iso.split("-").map((x) => Number.parseInt(x, 10));
63 const totalMonths = y * 12 + (m - 1) + (unit === "months" ? amount : amount * 12);
64 const ny = Math.floor(totalMonths / 12);
65 const nm = (totalMonths % 12) + 1;
66 const lastDay = new Date(Date.UTC(ny, nm, 0)).getUTCDate();
67 const nd = Math.min(d, lastDay);
68 const pad = (n: number) => String(n).padStart(2, "0");
69 return `${pad(ny) > "99" ? String(ny) : String(ny).padStart(4, "0")}-${pad(nm)}-${pad(nd)}`;
70}
71
72
73export function inWindow(value: string, start: string, end: string): boolean {
74 const v = utcDay(value);
75 return v >= utcDay(start) && v <= utcDay(end);
76}
77
78
79export function dateBucket(value: string, granularity: "day" | "week" | "month"): string {
80 const iso = value.length === 10 ? value : value.slice(0, 10);
81 if (granularity === "day") return iso;
82 if (granularity === "month") return iso.slice(0, 7);
83
84 const d = new Date(iso + "T00:00:00Z");
85 const dow = (d.getUTCDay() + 6) % 7;
86 const monday = new Date(d.getTime() - dow * DAY_MS);
87 return monday.toISOString().slice(0, 10);
88}
89