From dc11e077bfd75f9f58524180c639d96161db3e7d Mon Sep 17 00:00:00 2001 From: Paolo Donadeo Date: Tue, 19 May 2026 23:56:54 +0200 Subject: [PATCH] feat(timezone): add Windows timezone name resolution Add a `Windows_tz` module containing a CLDR-sourced mapping table from Windows timezone names to canonical IANA names. Update `timedesc_of_timestamp` to resolve Windows-style timezone identifiers (e.g. `"W. Europe Standard Time"`) via this table before constructing a `Timedesc.Time_zone.t`, falling back to the local timezone with a warning if resolution fails entirely. --- bin/dune | 2 +- bin/utils.ml | 13 +++- bin/windows_tz.ml | 149 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 bin/windows_tz.ml diff --git a/bin/dune b/bin/dune index 27ef7db..6ef7e63 100644 --- a/bin/dune +++ b/bin/dune @@ -1,7 +1,7 @@ (executable (public_name remind_sync) (name main) - (modules main commandLine remind eventPredicates utils) + (modules main commandLine remind eventPredicates utils windows_tz) (preprocess (pps ppx_deriving.show)) (libraries diff --git a/bin/utils.ml b/bin/utils.ml index b60bba9..eb1f6e7 100644 --- a/bin/utils.ml +++ b/bin/utils.ml @@ -105,7 +105,18 @@ let timedesc_of_timestamp (ts : timestamp) : Timedesc.t = | `With_tzid (ts, (_b, tz_name)) -> (* The timestamp is stored as if it were UTC but must be interpreted in tz_name. We reconstruct the wall-clock time in tz_name, then convert to target_tz. *) - let tz = Timedesc.Time_zone.make_exn tz_name in + (* Resolve the timezone name: Windows names (e.g. "W. Europe Standard Time") are + mapped to IANA via the CLDR table; otherwise the name is used as-is (assumed + to already be a valid IANA name). If resolution fails entirely, fall back to + target_tz with a warning. *) + let tz = + let candidate = Option.value ~default:tz_name (Windows_tz.to_iana tz_name) in + match Timedesc.Time_zone.make candidate with + | Some tz -> tz + | None -> + Printf.eprintf "Warning: unresolvable timezone %S, falling back to local timezone\n" tz_name; + !target_tz + in let wrong_ts = Timedesc.Utils.timestamp_of_ptime ts in let date = Timedesc.date (Timedesc.of_timestamp_exn ~tz_of_date_time:Timedesc.Time_zone.utc wrong_ts) in let year, month, day = (Timedesc.Date.year date, Timedesc.Date.month date, Timedesc.Date.day date) in diff --git a/bin/windows_tz.ml b/bin/windows_tz.ml new file mode 100644 index 0000000..be1c96e --- /dev/null +++ b/bin/windows_tz.ml @@ -0,0 +1,149 @@ +(** Mapping from Windows timezone names to canonical IANA timezone names. Source: Unicode CLDR + common/supplemental/windowsZones.xml, territory="001" entries. + https://github.com/unicode-org/cldr/blob/main/common/supplemental/windowsZones.xml *) + +let table : (string * string) list = + [ + ("AUS Central Standard Time", "Australia/Darwin"); + ("AUS Eastern Standard Time", "Australia/Sydney"); + ("Afghanistan Standard Time", "Asia/Kabul"); + ("Alaskan Standard Time", "America/Anchorage"); + ("Aleutian Standard Time", "America/Adak"); + ("Altai Standard Time", "Asia/Barnaul"); + ("Arab Standard Time", "Asia/Riyadh"); + ("Arabian Standard Time", "Asia/Dubai"); + ("Arabic Standard Time", "Asia/Baghdad"); + ("Argentina Standard Time", "America/Buenos_Aires"); + ("Astrakhan Standard Time", "Europe/Astrakhan"); + ("Atlantic Standard Time", "America/Halifax"); + ("Aus Central W. Standard Time", "Australia/Eucla"); + ("Azerbaijan Standard Time", "Asia/Baku"); + ("Azores Standard Time", "Atlantic/Azores"); + ("Bahia Standard Time", "America/Bahia"); + ("Bangladesh Standard Time", "Asia/Dhaka"); + ("Belarus Standard Time", "Europe/Minsk"); + ("Bougainville Standard Time", "Pacific/Bougainville"); + ("Canada Central Standard Time", "America/Regina"); + ("Cape Verde Standard Time", "Atlantic/Cape_Verde"); + ("Caucasus Standard Time", "Asia/Yerevan"); + ("Cen. Australia Standard Time", "Australia/Adelaide"); + ("Central America Standard Time", "America/Guatemala"); + ("Central Asia Standard Time", "Asia/Bishkek"); + ("Central Brazilian Standard Time", "America/Cuiaba"); + ("Central Europe Standard Time", "Europe/Budapest"); + ("Central European Standard Time", "Europe/Warsaw"); + ("Central Pacific Standard Time", "Pacific/Guadalcanal"); + ("Central Standard Time (Mexico)", "America/Mexico_City"); + ("Central Standard Time", "America/Chicago"); + ("Chatham Islands Standard Time", "Pacific/Chatham"); + ("China Standard Time", "Asia/Shanghai"); + ("Cuba Standard Time", "America/Havana"); + ("Dateline Standard Time", "Etc/GMT+12"); + ("E. Africa Standard Time", "Africa/Nairobi"); + ("E. Australia Standard Time", "Australia/Brisbane"); + ("E. Europe Standard Time", "Europe/Chisinau"); + ("E. South America Standard Time", "America/Sao_Paulo"); + ("Easter Island Standard Time", "Pacific/Easter"); + ("Eastern Standard Time (Mexico)", "America/Cancun"); + ("Eastern Standard Time", "America/New_York"); + ("Egypt Standard Time", "Africa/Cairo"); + ("Ekaterinburg Standard Time", "Asia/Yekaterinburg"); + ("FLE Standard Time", "Europe/Kiev"); + ("Fiji Standard Time", "Pacific/Fiji"); + ("GMT Standard Time", "Europe/London"); + ("GTB Standard Time", "Europe/Bucharest"); + ("Georgian Standard Time", "Asia/Tbilisi"); + ("Greenland Standard Time", "America/Godthab"); + ("Greenwich Standard Time", "Atlantic/Reykjavik"); + ("Haiti Standard Time", "America/Port-au-Prince"); + ("Hawaiian Standard Time", "Pacific/Honolulu"); + ("India Standard Time", "Asia/Calcutta"); + ("Iran Standard Time", "Asia/Tehran"); + ("Israel Standard Time", "Asia/Jerusalem"); + ("Jordan Standard Time", "Asia/Amman"); + ("Kaliningrad Standard Time", "Europe/Kaliningrad"); + ("Korea Standard Time", "Asia/Seoul"); + ("Libya Standard Time", "Africa/Tripoli"); + ("Line Islands Standard Time", "Pacific/Kiritimati"); + ("Lord Howe Standard Time", "Australia/Lord_Howe"); + ("Magadan Standard Time", "Asia/Magadan"); + ("Magallanes Standard Time", "America/Punta_Arenas"); + ("Marquesas Standard Time", "Pacific/Marquesas"); + ("Mauritius Standard Time", "Indian/Mauritius"); + ("Middle East Standard Time", "Asia/Beirut"); + ("Montevideo Standard Time", "America/Montevideo"); + ("Morocco Standard Time", "Africa/Casablanca"); + ("Mountain Standard Time (Mexico)", "America/Mazatlan"); + ("Mountain Standard Time", "America/Denver"); + ("Myanmar Standard Time", "Asia/Rangoon"); + ("N. Central Asia Standard Time", "Asia/Novosibirsk"); + ("Namibia Standard Time", "Africa/Windhoek"); + ("Nepal Standard Time", "Asia/Katmandu"); + ("New Zealand Standard Time", "Pacific/Auckland"); + ("Newfoundland Standard Time", "America/St_Johns"); + ("Norfolk Standard Time", "Pacific/Norfolk"); + ("North Asia East Standard Time", "Asia/Irkutsk"); + ("North Asia Standard Time", "Asia/Krasnoyarsk"); + ("North Korea Standard Time", "Asia/Pyongyang"); + ("Omsk Standard Time", "Asia/Omsk"); + ("Pacific SA Standard Time", "America/Santiago"); + ("Pacific Standard Time (Mexico)", "America/Tijuana"); + ("Pacific Standard Time", "America/Los_Angeles"); + ("Pakistan Standard Time", "Asia/Karachi"); + ("Paraguay Standard Time", "America/Asuncion"); + ("Qyzylorda Standard Time", "Asia/Qyzylorda"); + ("Romance Standard Time", "Europe/Paris"); + ("Russia Time Zone 10", "Asia/Srednekolymsk"); + ("Russia Time Zone 11", "Asia/Kamchatka"); + ("Russia Time Zone 3", "Europe/Samara"); + ("Russian Standard Time", "Europe/Moscow"); + ("SA Eastern Standard Time", "America/Cayenne"); + ("SA Pacific Standard Time", "America/Bogota"); + ("SA Western Standard Time", "America/La_Paz"); + ("SE Asia Standard Time", "Asia/Bangkok"); + ("Saint Pierre Standard Time", "America/Miquelon"); + ("Sakhalin Standard Time", "Asia/Sakhalin"); + ("Samoa Standard Time", "Pacific/Apia"); + ("Sao Tome Standard Time", "Africa/Sao_Tome"); + ("Saratov Standard Time", "Europe/Saratov"); + ("Singapore Standard Time", "Asia/Singapore"); + ("South Africa Standard Time", "Africa/Johannesburg"); + ("South Sudan Standard Time", "Africa/Juba"); + ("Sri Lanka Standard Time", "Asia/Colombo"); + ("Sudan Standard Time", "Africa/Khartoum"); + ("Syria Standard Time", "Asia/Damascus"); + ("Taipei Standard Time", "Asia/Taipei"); + ("Tasmania Standard Time", "Australia/Hobart"); + ("Tocantins Standard Time", "America/Araguaina"); + ("Tokyo Standard Time", "Asia/Tokyo"); + ("Tomsk Standard Time", "Asia/Tomsk"); + ("Tonga Standard Time", "Pacific/Tongatapu"); + ("Transbaikal Standard Time", "Asia/Chita"); + ("Turkey Standard Time", "Europe/Istanbul"); + ("Turks And Caicos Standard Time", "America/Grand_Turk"); + ("US Eastern Standard Time", "America/Indianapolis"); + ("US Mountain Standard Time", "America/Phoenix"); + ("UTC", "Etc/UTC"); + ("UTC+12", "Etc/GMT-12"); + ("UTC+13", "Etc/GMT-13"); + ("UTC-02", "Etc/GMT+2"); + ("UTC-08", "Etc/GMT+8"); + ("UTC-09", "Etc/GMT+9"); + ("UTC-11", "Etc/GMT+11"); + ("Ulaanbaatar Standard Time", "Asia/Ulaanbaatar"); + ("Venezuela Standard Time", "America/Caracas"); + ("Vladivostok Standard Time", "Asia/Vladivostok"); + ("Volgograd Standard Time", "Europe/Volgograd"); + ("W. Australia Standard Time", "Australia/Perth"); + ("W. Central Africa Standard Time", "Africa/Lagos"); + ("W. Europe Standard Time", "Europe/Berlin"); + ("W. Mongolia Standard Time", "Asia/Hovd"); + ("West Asia Standard Time", "Asia/Tashkent"); + ("West Bank Standard Time", "Asia/Hebron"); + ("West Pacific Standard Time", "Pacific/Port_Moresby"); + ("Yakutsk Standard Time", "Asia/Yakutsk"); + ("Yukon Standard Time", "America/Whitehorse"); + ] + +(** Look up a Windows timezone name and return the canonical IANA name, if known. *) +let to_iana (windows_name : string) : string option = List.assoc_opt windows_name table