brackt/app/lib/overnight-pause.ts

97 lines
3.2 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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
* startHHMMendHHMM 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;
}