From 7603c3eb21bd8c4ac2af2485b60e7fe56d36a0e3 Mon Sep 17 00:00:00 2001 From: JCEEE <0xjceee@proton.me> Date: Tue, 4 Aug 2026 08:46:39 +0100 Subject: [PATCH] add timezone file` --- timezone.ts | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 timezone.ts diff --git a/timezone.ts b/timezone.ts new file mode 100644 index 0000000..a91c10f --- /dev/null +++ b/timezone.ts @@ -0,0 +1,164 @@ +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", +];