165 lines
4.4 KiB
TypeScript
165 lines
4.4 KiB
TypeScript
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<string, number> = {
|
|
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",
|
|
];
|