branch: elpa/datetime commit 8cfa779858187c6149cf0b4048005f59ebac271b Author: Paul Pogonyshev <pogonys...@gmail.com> Commit: Paul Pogonyshev <pogonys...@gmail.com>
Improve system timezone determining; especially important for Windows. --- datetime.el | 10 +++++++--- dev/HarvestData.java | 46 +++++++++++++++++++++++++++++++++++++++++++++- test/base.el | 23 +++++++++++++++++++++++ timezone-data.extmap | Bin 900579 -> 902807 bytes 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/datetime.el b/datetime.el index c7bd600817..7b61037f69 100644 --- a/datetime.el +++ b/datetime.el @@ -305,8 +305,12 @@ form: "?")))) (if (extmap-contains-key datetime--timezone-extmap system-timezone) system-timezone - (error "Failed to determine system timezone%s; consider customizing `datetime-timezone' variable" - (if (eq system-timezone '\?) "" (format-message " (found raw value: `%s')" system-timezone))))))) + (let* ((aliases (extmap-get datetime--timezone-extmap :aliases t)) + (entry (assq system-timezone aliases))) + (if entry + (cdr entry) + (error "Failed to determine system timezone%s; consider customizing `datetime-timezone' variable" + (if (eq system-timezone '\?) "" (format-message " (found raw value: `%s')" system-timezone))))))))) (defun datetime--parse-pattern (type pattern options) @@ -2064,7 +2068,7 @@ create based on timezones `datetime' knows about and their rules. Locale-specific timezone names are contained in a different database. See `datetime-timezone-name-database-version'." - 7) + 8) (defun datetime-timezone-name-database-version () "Return timezone name database version, a simple integer. diff --git a/dev/HarvestData.java b/dev/HarvestData.java index 4b681db9d8..59a71c4d0e 100644 --- a/dev/HarvestData.java +++ b/dev/HarvestData.java @@ -325,7 +325,13 @@ public class HarvestData protected static void printTimezoneData () throws Exception { - Map <ZoneId, List <Object>> data = new LinkedHashMap <> (); + var data = new LinkedHashMap <ZoneId, List <Object>> (); + var aliases = new LinkedHashMap <String, Set <ZoneId>> (); + var timezones_with_matching_abbreviations = new HashSet <ZoneId> (); + var abbreviation_retriever = DateTimeFormatter.ofPattern ("z", Locale.ENGLISH); + var utc_formatter = DateTimeFormatter.ofPattern ("yyyy-MM-dd HH:mm:ss z"); + Instant[] abbreviations_at = { Instant.from (utc_formatter.parse ("2020-01-01 00:00:00 UTC")), + Instant.from (utc_formatter.parse ("2020-07-01 00:00:00 UTC")) }; for (ZoneId timezone : getAllTimezones ()) { ZoneRules rules = timezone.getRules (); @@ -434,12 +440,50 @@ public class HarvestData zone_data.add (toLispList (transition_rule_data)); data.put (timezone, zone_data); + + for (var at : abbreviations_at) { + var abbreviation = abbreviation_retriever.format (ZonedDateTime.ofInstant (at, timezone)); + if (abbreviation.equals (timezone.getId ())) + timezones_with_matching_abbreviations.add (timezone); + else + aliases.computeIfAbsent (abbreviation, __ -> new HashSet <> ()).add (timezone); + } } } + // When computing timezone alias map we use these rules: + // - if timezone identifier matches its normal abbreviation, its DST abbreviation + // becomes an alias (example: "CEST" becomes an alias for "CET"); + // - else both abbreviations become an alias for the timezone, but only if they + // don't clash with anything else (example: "EGT" and "EGST" are both aliases + // for "America/Scoresbysund"). + var timezones_with_conflicting_aliases = new HashSet <ZoneId> (); + for (var timezones : aliases.values ()) { + if (timezones.size () > 1) + timezones_with_conflicting_aliases.addAll (timezones); + } + + timezones_with_conflicting_aliases.removeAll (timezones_with_matching_abbreviations); + + for (var it = aliases.values ().iterator (); it.hasNext ();) { + var with_this_alias = it.next (); + with_this_alias.removeAll (timezones_with_conflicting_aliases); + if (with_this_alias.size () != 1) + it.remove (); + } + System.out.println ("("); + for (Map.Entry <ZoneId, List <Object>> entry : data.entrySet ()) System.out.format ("(%s\n %s)\n", entry.getKey (), entry.getValue ().stream ().map (String::valueOf).collect (Collectors.joining ("\n "))); + + // Aliases don't go into names: they are relatively few and are not used for + // formatting or parsing, currently only when determining OS timezone. + System.out.format ("(:aliases\n %s)\n", + aliases.entrySet ().stream () + .map ((entry) -> String.format ("(%s . %s)", entry.getKey (), entry.getValue ().iterator ().next ().getId ())) + .collect (Collectors.joining ("\n "))); + System.out.println (")"); } diff --git a/test/base.el b/test/base.el index c20b1e5df9..c45682a075 100644 --- a/test/base.el +++ b/test/base.el @@ -89,6 +89,24 @@ (datetime--test-set-up-parser datetime--test-timezone datetime--test-locale datetime--test-pattern ,@body))) +;; Copied from Eldev source code, see documentation there. +(defmacro datetime--advised (spec &rest body) + (declare (indent 1) (debug (sexp body))) + (let ((symbol (nth 0 spec)) + (where (nth 1 spec)) + (function (nth 2 spec)) + (props (nthcdr 3 spec)) + (fn (make-symbol "$fn"))) + `(let ((,fn ,function)) + (when ,fn + (if (advice-member-p ,fn ,symbol) + (setf ,fn nil) + (advice-add ,symbol ,where ,fn ,@props))) + (unwind-protect + ,(macroexp-progn body) + (when ,fn + (advice-remove ,symbol ,fn)))))) + (defun datetime--test (command times) (unless (listp times) (setq times (list times))) @@ -211,5 +229,10 @@ am-pm = %S" (dotimes (k length) (should (stringp (aref value k)))))))))) +(ert-deftest datetime--determine-system-timezone () + (datetime--advised ('current-time-zone :override (lambda () '(7200 "CEST"))) + (let ((system-type 'this-system-type-is-not-specialcase)) + (should (eq (datetime--determine-system-timezone) 'CET))))) + (provide 'test/base) diff --git a/timezone-data.extmap b/timezone-data.extmap index 731fd92781..fbac550399 100644 Binary files a/timezone-data.extmap and b/timezone-data.extmap differ