// timezones.ts /** * Mapping of Windows Timezone names to IANA Timezone names. * Sourced from Unicode CLDR data. */ export const WINDOWS_TO_IANA: Record = { "Dateline Standard Time": "Etc/GMT+12", "UTC-11": "Etc/GMT+11", "Hawaiian Standard Time": "Pacific/Honolulu", "Alaskan Standard Time": "America/Anchorage", "Pacific Standard Time (Mexico)": "America/Santa_Isabel", "Pacific Standard Time": "America/Los_Angeles", "US Mountain Standard Time": "America/Phoenix", "Mountain Standard Time (Mexico)": "America/Chihuahua", "Mountain Standard Time": "America/Denver", "Central America Standard Time": "America/Guatemala", "Central Standard Time": "America/Chicago", "Central Standard Time (Mexico)": "America/Mexico_City", "Canada Central Standard Time": "America/Regina", "SA Pacific Standard Time": "America/Bogota", "Eastern Standard Time": "America/New_York", "US Eastern Standard Time": "America/Indiana/Indianapolis", "Venezuela Standard Time": "America/Caracas", "Paraguay Standard Time": "America/Asuncion", "Atlantic Standard Time": "America/Halifax", "Central Brazilian Standard Time": "America/Cuiaba", "SA Western Standard Time": "America/La_Paz", "Pacific SA Standard Time": "America/Santiago", "Newfoundland Standard Time": "America/St_Johns", "E. South America Standard Time": "America/Sao_Paulo", "Argentina Standard Time": "America/Buenos_Aires", "SA Eastern Standard Time": "America/Cayenne", "Greenland Standard Time": "America/Godthab", "Montevideo Standard Time": "America/Montevideo", "Bahia Standard Time": "America/Bahia", "Azores Standard Time": "Atlantic/Azores", "Cape Verde Standard Time": "Atlantic/Cape_Verde", "Morocco Standard Time": "Africa/Casablanca", "GMT Standard Time": "Europe/London", "Greenwich Standard Time": "Atlantic/Reykjavik", "W. Europe Standard Time": "Europe/Berlin", "Central Europe Standard Time": "Europe/Budapest", "Romance Standard Time": "Europe/Paris", "Central European Standard Time": "Europe/Warsaw", "W. Central Africa Standard Time": "Africa/Lagos", "Namibia Standard Time": "Africa/Windhoek", "Jordan Standard Time": "Asia/Amman", "GTB Standard Time": "Europe/Bucharest", "Middle East Standard Time": "Asia/Beirut", "Egypt Standard Time": "Africa/Cairo", "Syria Standard Time": "Asia/Damascus", "E. Europe Standard Time": "Europe/Chisinau", "South Africa Standard Time": "Africa/Johannesburg", "FLE Standard Time": "Europe/Kiev", "Turkey Standard Time": "Europe/Istanbul", "Israel Standard Time": "Asia/Jerusalem", "Kaliningrad Standard Time": "Europe/Kaliningrad", "Libya Standard Time": "Africa/Tripoli", "Arabic Standard Time": "Asia/Baghdad", "Arab Standard Time": "Asia/Riyadh", "Belarus Standard Time": "Europe/Minsk", "Russian Standard Time": "Europe/Moscow", "E. Africa Standard Time": "Africa/Nairobi", "Iran Standard Time": "Asia/Tehran", "Arabian Standard Time": "Asia/Dubai", "Azerbaijan Standard Time": "Asia/Baku", "Russia Time Zone 3": "Europe/Samara", "Mauritius Standard Time": "Indian/Mauritius", "Georgian Standard Time": "Asia/Tbilisi", "Caucasus Standard Time": "Asia/Yerevan", "Afghanistan Standard Time": "Asia/Kabul", "West Asia Standard Time": "Asia/Tashkent", "Ekaterinburg Standard Time": "Asia/Yekaterinburg", "Pakistan Standard Time": "Asia/Karachi", "India Standard Time": "Asia/Kolkata", "Sri Lanka Standard Time": "Asia/Colombo", "Nepal Standard Time": "Asia/Kathmandu", "Central Asia Standard Time": "Asia/Almaty", "Bangladesh Standard Time": "Asia/Dhaka", "N. Central Asia Standard Time": "Asia/Novosibirsk", "Myanmar Standard Time": "Asia/Rangoon", "SE Asia Standard Time": "Asia/Bangkok", "North Asia Standard Time": "Asia/Krasnoyarsk", "China Standard Time": "Asia/Shanghai", "North Asia East Standard Time": "Asia/Irkutsk", "Singapore Standard Time": "Asia/Singapore", "W. Australia Standard Time": "Australia/Perth", "Taipei Standard Time": "Asia/Taipei", "Ulaanbaatar Standard Time": "Asia/Ulaanbaatar", "Tokyo Standard Time": "Asia/Tokyo", "Korea Standard Time": "Asia/Seoul", "Yakutsk Standard Time": "Asia/Yakutsk", "Cen. Australia Standard Time": "Australia/Adelaide", "AUS Central Standard Time": "Australia/Darwin", "E. Australia Standard Time": "Australia/Brisbane", "AUS Eastern Standard Time": "Australia/Sydney", "West Pacific Standard Time": "Pacific/Port_Moresby", "Tasmania Standard Time": "Australia/Hobart", "Magadan Standard Time": "Asia/Magadan", "Vladivostok Standard Time": "Asia/Vladivostok", "Russia Time Zone 10": "Asia/Srednekolymsk", "Central Pacific Standard Time": "Pacific/Guadalcanal", "Russia Time Zone 11": "Asia/Anadyr", "New Zealand Standard Time": "Pacific/Auckland", "Fiji Standard Time": "Pacific/Fiji", "Tonga Standard Time": "Pacific/Tongatapu", "Samoa Standard Time": "Pacific/Apia", "Line Islands Standard Time": "Pacific/Kiritimati" }; /** * Resolves an IANA timezone name from a given TZID string. * Supports Windows timezone names, direct IANA names, and UTC. */ export function resolveIanaName(tzid: string): string | null { if (!tzid || tzid === "UTC" || tzid === "None") return "UTC"; // Heuristic: IANA names typically include a forward slash if (tzid.includes("/")) return tzid; return WINDOWS_TO_IANA[tzid] ?? null; } /** * Returns the UTC offset in milliseconds for a given IANA timezone at a * specific point in time. Positive = ahead of UTC, negative = behind UTC. * e.g. "America/New_York" in summer -> -14400000 (-4h) */ export function getUtcOffsetMs(ianaName: string, atDate: Date): number { // Trick: format the same instant in UTC and in the target zone, // parse both, and subtract. // "en-CA" produces "YYYY-MM-DD, HH:MM:SS" (unambiguous) const options: Intl.DateTimeFormatOptions = { timeZone: "UTC", hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", }; const utcFormatter = new Intl.DateTimeFormat("en-CA", options); const localFormatter = new Intl.DateTimeFormat("en-CA", { ...options, timeZone: ianaName }); const formatToIso = (formatter: Intl.DateTimeFormat, date: Date) => { return formatter.format(date).replace(", ", "T"); }; const utcStr = formatToIso(utcFormatter, atDate); const localStr = formatToIso(localFormatter, atDate); const utcMs = new Date(utcStr + "Z").getTime(); const localMs = new Date(localStr + "Z").getTime(); return localMs - utcMs; }