forked from GitHubMirrors/silverbullet-icalendar
conductor(checkpoint): Checkpoint end of Phase 1: Foundation
This commit is contained in:
159
timezones.ts
Normal file
159
timezones.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
// timezones.ts
|
||||
|
||||
/**
|
||||
* Mapping of Windows Timezone names to IANA Timezone names.
|
||||
* Sourced from Unicode CLDR data.
|
||||
*/
|
||||
export const WINDOWS_TO_IANA: Record<string, string> = {
|
||||
"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;
|
||||
}
|
||||
43
timezones_test.ts
Normal file
43
timezones_test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { assertEquals } from "jsr:@std/assert";
|
||||
import { resolveIanaName, getUtcOffsetMs } from "./timezones.ts";
|
||||
|
||||
Deno.test("resolveIanaName - Windows names", () => {
|
||||
assertEquals(resolveIanaName("Eastern Standard Time"), "America/New_York");
|
||||
assertEquals(resolveIanaName("Romance Standard Time"), "Europe/Paris");
|
||||
assertEquals(resolveIanaName("Pacific Standard Time"), "America/Los_Angeles");
|
||||
});
|
||||
|
||||
Deno.test("resolveIanaName - IANA names (identity)", () => {
|
||||
assertEquals(resolveIanaName("America/Chicago"), "America/Chicago");
|
||||
assertEquals(resolveIanaName("Europe/London"), "Europe/London");
|
||||
});
|
||||
|
||||
Deno.test("resolveIanaName - UTC and special cases", () => {
|
||||
assertEquals(resolveIanaName("UTC"), "UTC");
|
||||
assertEquals(resolveIanaName("None"), "UTC");
|
||||
assertEquals(resolveIanaName(""), "UTC");
|
||||
});
|
||||
|
||||
Deno.test("resolveIanaName - Unknown names", () => {
|
||||
assertEquals(resolveIanaName("Mars Standard Time"), null);
|
||||
});
|
||||
|
||||
Deno.test("getUtcOffsetMs - New York (DST check)", () => {
|
||||
const jan = new Date("2025-01-15T12:00:00Z");
|
||||
const july = new Date("2025-07-15T12:00:00Z");
|
||||
|
||||
// America/New_York is UTC-5 in Winter
|
||||
assertEquals(getUtcOffsetMs("America/New_York", jan), -5 * 3600000);
|
||||
// America/New_York is UTC-4 in Summer
|
||||
assertEquals(getUtcOffsetMs("America/New_York", july), -4 * 3600000);
|
||||
});
|
||||
|
||||
Deno.test("getUtcOffsetMs - Paris (DST check)", () => {
|
||||
const jan = new Date("2025-01-15T12:00:00Z");
|
||||
const july = new Date("2025-07-15T12:00:00Z");
|
||||
|
||||
// Europe/Paris is UTC+1 in Winter
|
||||
assertEquals(getUtcOffsetMs("Europe/Paris", jan), 1 * 3600000);
|
||||
// Europe/Paris is UTC+2 in Summer
|
||||
assertEquals(getUtcOffsetMs("Europe/Paris", july), 2 * 3600000);
|
||||
});
|
||||
Reference in New Issue
Block a user