/** * Utilities for overnight pause windows. * Works in both Node.js and browser — uses only Intl, no extra dependencies. */ function getLocalMinutes(timezone: string, now: Date): number { const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone, hour: "2-digit", minute: "2-digit", hour12: false, }); const parts = formatter.formatToParts(now); const hour = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10); const minute = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10); // Intl hour12:false can return "24" for midnight — normalize to 0 return (hour === 24 ? 0 : hour) * 60 + minute; } /** * Build a UTC Date representing "the given date at HH:MM in the given timezone." * Corrects for the UTC offset by comparing what local time the naive UTC produces. */ function utcForLocalTime( timezone: string, year: number, month: number, // 1-indexed day: number, hour: number, minute: number ): Date { // Start with a naive UTC (treating target H:M as UTC) const approxUTC = Date.UTC(year, month - 1, day, hour, minute, 0); const probe = new Date(approxUTC); const localMins = getLocalMinutes(timezone, probe); const targetMins = hour * 60 + minute; // Shift: if local is behind target, add the gap; if ahead, subtract it return new Date(approxUTC + (targetMins - localMins) * 60 * 1000); } /** * Returns true if `now` falls inside the overnight pause window defined by * startHHMM–endHHMM in the given IANA timezone. * * The window almost always spans midnight (e.g. "23:00"–"07:00"), but a * same-day window (e.g. "10:00"–"18:00") is also handled correctly. */ export function isInOvernightWindow( timezone: string, startHHMM: string, endHHMM: string, now = new Date() ): boolean { const currentMinutes = getLocalMinutes(timezone, now); const [sh, sm] = startHHMM.split(":").map(Number); const [eh, em] = endHHMM.split(":").map(Number); const startMinutes = sh * 60 + sm; const endMinutes = eh * 60 + em; if (startMinutes > endMinutes) { // Spans midnight: in window if current >= start OR current < end return currentMinutes >= startMinutes || currentMinutes < endMinutes; } // Same-day window: in window if start <= current < end return currentMinutes >= startMinutes && currentMinutes < endMinutes; } /** * Returns the next UTC Date when endHHMM will occur in the given timezone. * Used to tell clients when the overnight window will end. */ export function getOvernightResumeUTC( timezone: string, endHHMM: string, now = new Date() ): Date { const [eh, em] = endHHMM.split(":").map(Number); // Get current calendar date in the target timezone const dateFmt = new Intl.DateTimeFormat("en-CA", { timeZone: timezone, year: "numeric", month: "2-digit", day: "2-digit", }); const [year, month, day] = dateFmt.format(now).split("-").map(Number); // Candidate: today at endHHMM in the target timezone const candidate = utcForLocalTime(timezone, year, month, day, eh, em); // If this moment has already passed, advance one day if (candidate.getTime() <= now.getTime()) { return new Date(candidate.getTime() + 24 * 60 * 60 * 1000); } return candidate; }