Files
SixFlagsSuperCalendar/lib/env.ts
T
josh 06b911917d
Build and Deploy / Build & Push (push) Successful in 2m50s
fix: use explicit Eastern timezone for day boundary instead of system TZ
getTodayLocal() relied on system clock hours, which broke in the web
container (TZ defaulting to UTC) — the day flipped at 11 PM EDT (3 AM
UTC) instead of 3 AM Eastern. Now uses Intl.DateTimeFormat with an
explicit America/New_York timezone. Also replaced all toISOString()
date formatting with local-component helpers to avoid UTC conversion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 23:11:33 -04:00

126 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Environment variable helpers.
*/
/**
* Parse a staleness window from an env var string (interpreted as hours).
* Falls back to `defaultHours` when the value is missing, non-numeric,
* non-finite, or <= 0 — preventing NaN from silently breaking staleness checks.
*/
export function parseStalenessHours(envVar: string | undefined, defaultHours: number): number {
const parsed = parseInt(envVar ?? "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultHours;
}
const APP_TIMEZONE = "America/New_York";
/**
* Returns today's date as YYYY-MM-DD in Eastern time with a 3 AM switchover.
* Uses Intl.DateTimeFormat so it works regardless of the system/container TZ.
*/
export function getTodayLocal(): string {
const now = new Date();
const fmt = new Intl.DateTimeFormat("en-US", {
timeZone: APP_TIMEZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "numeric",
hour12: false,
});
const parts = fmt.formatToParts(now);
const hour = parseInt(parts.find((p) => p.type === "hour")!.value, 10) % 24;
const target = hour < 3 ? new Date(now.getTime() - 86_400_000) : now;
return formatDateTZ(target, APP_TIMEZONE);
}
/**
* Format a Date as YYYY-MM-DD in a specific IANA timezone.
*/
export function formatDateTZ(d: Date, tz: string): string {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: tz,
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(d);
const y = parts.find((p) => p.type === "year")!.value;
const m = parts.find((p) => p.type === "month")!.value;
const day = parts.find((p) => p.type === "day")!.value;
return `${y}-${m}-${day}`;
}
/**
* Format a Date as YYYY-MM-DD using its local (system-timezone) components.
* Use this instead of d.toISOString().slice(0,10) which converts to UTC.
*/
export function formatDateLocal(d: Date): string {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
/**
* Returns the short timezone abbreviation for a given IANA timezone,
* e.g. "America/Los_Angeles" → "PDT" or "PST".
*/
export function getTimezoneAbbr(timezone: string): string {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
timeZoneName: "short",
}).formatToParts(new Date());
return parts.find((p) => p.type === "timeZoneName")?.value ?? "";
}
/**
* Returns true when the current time in the park's timezone is within
* the operating window (open time through 1 hour after close), based on
* a hoursLabel like "10am 6pm". Falls back to true when unparseable.
*
* Uses the park's IANA timezone so a Pacific park's "10am" is correctly
* compared to Pacific time regardless of where the server is running.
*/
export function isWithinOperatingWindow(hoursLabel: string, timezone: string): boolean {
return getOperatingStatus(hoursLabel, timezone) !== "closed";
}
/**
* Returns the park's current operating status relative to its scheduled hours:
* "open" — within the scheduled open window
* "closing" — past scheduled close but within the 1-hour wind-down buffer
* "closed" — outside the window entirely
* Falls back to "open" when the label can't be parsed.
*/
export function getOperatingStatus(hoursLabel: string, timezone: string): "open" | "closing" | "closed" {
const m = hoursLabel.match(
/^(\d+)(?::(\d+))?(am|pm)\s*[-]\s*(\d+)(?::(\d+))?(am|pm)$/i
);
if (!m) return "open";
const toMinutes = (h: string, min: string | undefined, period: string) => {
let hours = parseInt(h, 10);
const minutes = min ? parseInt(min, 10) : 0;
if (period.toLowerCase() === "pm" && hours !== 12) hours += 12;
if (period.toLowerCase() === "am" && hours === 12) hours = 0;
return hours * 60 + minutes;
};
const openMin = toMinutes(m[1], m[2], m[3]);
const closeMin = toMinutes(m[4], m[5], m[6]);
// Get the current time in the park's local timezone.
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
minute: "2-digit",
hour12: false,
}).formatToParts(new Date());
const h = parseInt(parts.find((p) => p.type === "hour")?.value ?? "0", 10);
const min = parseInt(parts.find((p) => p.type === "minute")?.value ?? "0", 10);
const nowMin = (h % 24) * 60 + min;
if (nowMin >= openMin && nowMin <= closeMin) return "open";
if (nowMin > closeMin && nowMin <= closeMin + 60) return "closing";
return "closed";
}