export type RPClaimDateTimeValueType = "date" | "datetime"; export const FALLBACK_TIME_ZONE = "UTC"; export function getBrowserTimeZone(): string { return Intl.DateTimeFormat().resolvedOptions().timeZone || FALLBACK_TIME_ZONE; } export function getSupportedTimeZones(currentTimeZone = getBrowserTimeZone()) { const supported = typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : []; return Array.from( new Set([currentTimeZone, FALLBACK_TIME_ZONE, ...supported]), ); } function getTimeZoneOffsetMs(date: Date, timeZone: string) { const parts = new Intl.DateTimeFormat("en-US", { timeZone, hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }).formatToParts(date); const values = Object.fromEntries( parts .filter((part) => part.type !== "literal") .map((part) => [part.type, part.value]), ); const hour = values.hour === "24" ? "00" : values.hour; const asUTC = Date.UTC( Number(values.year), Number(values.month) - 1, Number(values.day), Number(hour), Number(values.minute), Number(values.second), ); return asUTC - date.getTime(); } function zonedDateTimeToUnixSeconds( year: number, month: number, day: number, hour: number, minute: number, timeZone: string, ) { const utcGuess = Date.UTC(year, month - 1, day, hour, minute, 0); let instant = utcGuess - getTimeZoneOffsetMs(new Date(utcGuess), timeZone); const corrected = utcGuess - getTimeZoneOffsetMs(new Date(instant), timeZone); if (corrected !== instant) { instant = corrected; } return Math.trunc(instant / 1000); } export function dateTimeInputToUnixSeconds( value: string, valueType: RPClaimDateTimeValueType, timeZone: string, ): number | null { const trimmed = value.trim(); const match = valueType === "date" ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed) : /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(trimmed); if (!match) return null; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const hour = valueType === "datetime" ? Number(match[4]) : 0; const minute = valueType === "datetime" ? Number(match[5]) : 0; const unixSeconds = zonedDateTimeToUnixSeconds( year, month, day, hour, minute, timeZone || FALLBACK_TIME_ZONE, ); return Number.isFinite(unixSeconds) ? unixSeconds : null; } export function unixSecondsToDateTimeInput( value: number, valueType: RPClaimDateTimeValueType, timeZone: string, ) { const date = new Date(value * 1000); if (Number.isNaN(date.getTime())) return ""; const parts = new Intl.DateTimeFormat("en-CA", { timeZone: timeZone || FALLBACK_TIME_ZONE, hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }).formatToParts(date); const values = Object.fromEntries( parts .filter((part) => part.type !== "literal") .map((part) => [part.type, part.value]), ); const hour = values.hour === "24" ? "00" : values.hour; const dateText = `${values.year}-${values.month}-${values.day}`; if (valueType === "date") return dateText; return `${dateText}T${hour}:${values.minute}`; } export function claimDateTimeValueToInputString( value: unknown, fallback: string, valueType: RPClaimDateTimeValueType, timeZone: string, ) { if (typeof value === "number" && Number.isFinite(value)) { return unixSecondsToDateTimeInput(value, valueType, timeZone); } if (typeof value === "string" && /^-?\d+$/.test(value.trim())) { return unixSecondsToDateTimeInput( Number(value.trim()), valueType, timeZone, ); } const text = typeof value === "string" ? value : fallback; return valueType === "date" ? text.slice(0, 10) : text.slice(0, 16); }