export const AUTO_TZ = "auto"; export const DEFAULT_TZ = "UTC"; export function resolveTz(tz?: string | null): string { if (tz && tz !== AUTO_TZ) return tz; try { return Intl.DateTimeFormat().resolvedOptions().timeZone || DEFAULT_TZ; } catch { return DEFAULT_TZ; } } export function dateStrInTz(date: Date, tz: string): string { const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(date); const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "00"; return `${get("year")}-${get("month")}-${get("day")}`; } export function weekdayInTz(date: Date, tz: string): number { const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, weekday: "short", }).formatToParts(date); const wd = parts.find((p) => p.type === "weekday")?.value ?? ""; const map: Record = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, }; return map[wd] ?? date.getDay(); } export function todayInTz(tz: string): string { return dateStrInTz(new Date(), tz); } export function addDaysStr(dateStr: string, days: number): string { const [y, m, d] = dateStr.split("-").map(Number); const dt = new Date(Date.UTC(y, m - 1, d + days)); return dt.toISOString().slice(0, 10); } export function weekStart(payday: number, tz: string): string { const today = todayInTz(tz); const wd = weekdayInTz(new Date(), tz); const back = ((wd - payday) % 7 + 7) % 7; return addDaysStr(today, -back); } function monthStartStr(): string { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-01`; } function monthEndStr(month?: string): string { if (!month) { const d = new Date(); month = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; } const [y, m] = month.split("-").map(Number); const lastDay = new Date(y, m, 0).getDate(); return `${month}-${String(lastDay).padStart(2, "0")}`; } export function periodStart(period: string, payday: number, tz: string): string { if (period === "daily") return todayInTz(tz); if (period === "weekly") return weekStart(payday, tz); if (period === "monthly") return monthStartStr(); return weekStart(payday, tz); } export function periodEnd(period: string, start: string): string { if (period === "daily") return start; if (period === "weekly") return addDaysStr(start, 6); if (period === "monthly") return monthEndStr(start.slice(0, 7)); return start; } export function wallClockToUtc(dateStr: string, time: string, tz: string): number { const [y, m, d] = dateStr.split("-").map(Number); const [h, min] = time.split(":").map(Number); let epoch = Date.UTC(y, m - 1, d, h, min); for (let i = 0; i < 4; i++) { const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }).formatToParts(new Date(epoch)); const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "00"; const gotDate = `${get("year")}-${get("month")}-${get("day")}`; let gotHour = parseInt(get("hour"), 10); if (gotHour === 24) gotHour = 0; const gotEpoch = Date.UTC( parseInt(get("year"), 10), parseInt(get("month"), 10) - 1, parseInt(get("day"), 10), gotHour, parseInt(get("minute"), 10), ); if (gotEpoch === epoch && gotDate === dateStr) break; epoch = Date.UTC(y, m - 1, d, h, min) + (epoch - gotEpoch); } return epoch; } export const COMMON_TIMEZONES: string[] = [ "Europe/London", "Europe/Paris", "Europe/Berlin", "Europe/Madrid", "Europe/Rome", "Europe/Amsterdam", "Europe/Lisbon", "Europe/Dublin", "Europe/Warsaw", "Europe/Stockholm", "Europe/Athens", "Europe/Istanbul", "Europe/Moscow", "America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "America/Toronto", "America/Vancouver", "America/Sao_Paulo", "America/Mexico_City", "Asia/Tokyo", "Asia/Shanghai", "Asia/Hong_Kong", "Asia/Singapore", "Asia/Seoul", "Asia/Kolkata", "Asia/Dubai", "Asia/Jerusalem", "Asia/Bangkok", "Asia/Manila", "Australia/Sydney", "Australia/Melbourne", "Australia/Brisbane", "Australia/Perth", "Australia/Adelaide", "Pacific/Auckland", "Africa/Johannesburg", "Africa/Cairo", "Africa/Lagos", "Africa/Nairobi", "UTC", ];