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.
This commit is contained in:
2026-05-19 23:56:54 +02:00
parent 481df20d0a
commit 922fd9a97e
3 changed files with 162 additions and 2 deletions

View File

@@ -1,7 +1,7 @@
(executable (executable
(public_name remind_sync) (public_name remind_sync)
(name main) (name main)
(modules main commandLine remind eventPredicates utils) (modules main commandLine remind eventPredicates utils windows_tz)
(preprocess (preprocess
(pps ppx_deriving.show)) (pps ppx_deriving.show))
(libraries (libraries

View File

@@ -105,7 +105,18 @@ let timedesc_of_timestamp (ts : timestamp) : Timedesc.t =
| `With_tzid (ts, (_b, tz_name)) -> | `With_tzid (ts, (_b, tz_name)) ->
(* The timestamp is stored as if it were UTC but must be interpreted in 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. *) 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 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 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 let year, month, day = (Timedesc.Date.year date, Timedesc.Date.month date, Timedesc.Date.day date) in

149
bin/windows_tz.ml Normal file
View File

@@ -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