// I-3 時間感知:所有作息判斷一律以 Asia/Taipei 當地時間為準,不受伺服器所在時區影響。 const TAIPEI_PARTS_FORMATTER = new Intl.DateTimeFormat("en-US", { timeZone: "Asia/Taipei", weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false, year: "numeric", month: "2-digit", day: "2-digit", }); const WEEKDAY_INDEX: Record = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, }; export interface TaipeiTimeParts { weekday: number; // 0=週日 ... 6=週六 hour: number; minute: number; year: number; month: number; day: number; } export function taipeiParts(date: Date): TaipeiTimeParts { const parts = TAIPEI_PARTS_FORMATTER.formatToParts(date); const get = (type: string) => parts.find((p) => p.type === type)?.value ?? ""; return { weekday: WEEKDAY_INDEX[get("weekday")] ?? 0, hour: Number(get("hour")), minute: Number(get("minute")), year: Number(get("year")), month: Number(get("month")), day: Number(get("day")), }; } export function minutesOfDay(date: Date): number { const { hour, minute } = taipeiParts(date); return hour * 60 + minute; } export function isSameTaipeiDate(a: Date, b: Date): boolean { const pa = taipeiParts(a); const pb = taipeiParts(b); return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day; } // 回傳「當地日期」的午夜時間點(以 UTC Date 物件表示,僅用於資料庫時間比較的下界)。 export function startOfTaipeiDay(date: Date): Date { const { year, month, day } = taipeiParts(date); // Asia/Taipei 為 UTC+8,當地 00:00 對應 UTC 前一天 16:00。 return new Date(Date.UTC(year, month - 1, day, -8, 0, 0, 0)); }