branch: elpa/datetime
commit ecc98a0b2d9b465c30f8417f2f532cc22881064b
Author: Paul Pogonyshev <pogonys...@gmail.com>
Commit: Paul Pogonyshev <pogonys...@gmail.com>

    Fix several issues that would sometimes cause the library to pick wrong 
timezone name variant (DST vs. normal).
---
 datetime.el          |   6 +++-
 dev/HarvestData.java |  87 ++++++++++++++++++++++++++++++++++++++++++---------
 test/format.el       |  30 +++++++++++++++---
 timezone-data.extmap | Bin 889713 -> 900579 bytes
 4 files changed, 104 insertions(+), 19 deletions(-)

diff --git a/datetime.el b/datetime.el
index 7a6308347f..6596a4d6e1 100644
--- a/datetime.el
+++ b/datetime.el
@@ -798,7 +798,11 @@ to this function.
                                        day-of-month -1))
                    (offset-before   (plist-get rule :before)))
               (unless transitions
-                (push offset-before transitions))
+                ;; Preserve our DST "flag" across year boundary.
+                (push (if (floatp (car (last (aref all-year-transitions (1- 
year-offset)))))
+                          (float offset-before)
+                        offset-before)
+                      transitions))
               (when day-of-week
                 (let ((current-weekday (% (+ year-day (aref 
datetime--gregorian-first-day-of-year (mod year 400))) 7)))
                   (setq year-day (if (< day-of-month 0) (- year-day (mod (- 
day-of-week current-weekday) 7)) (+ year-day (mod (- day-of-week 
current-weekday) 7))))))
diff --git a/dev/HarvestData.java b/dev/HarvestData.java
index 7eb866dc55..8735a5d523 100644
--- a/dev/HarvestData.java
+++ b/dev/HarvestData.java
@@ -298,26 +298,52 @@ public class HarvestData
                 LocalDateTime         first           = 
LocalDateTime.ofInstant (transitions.get (0).getInstant (), ZoneOffset.UTC);
                 int                   base_year       = Year.of (first.get 
(ChronoField.YEAR)).getValue ();
                 long                  base            = Year.of (first.get 
(ChronoField.YEAR)).atDay (1).atStartOfDay ().toInstant 
(ZoneOffset.UTC).getEpochSecond ();
-                int                   last_offset     = transitions.get 
(0).getOffsetBefore ().getTotalSeconds ();
+                Offset                last_offset     = new Offset 
(transitions.get (0).getOffsetBefore ().getTotalSeconds (), false);
                 List <Object>         zone_data       = new ArrayList <> ();
                 List <List <Object>>  transition_data = new ArrayList <> ();
 
-                for (ZoneOffsetTransition transition : transitions) {
-                    int  year_offset = (int) ((transition.getInstant 
().getEpochSecond () - base) / AVERAGE_SECONDS_IN_YEAR);
-                    if ((transition.getInstant ().getEpochSecond () + 1 - 
base) % AVERAGE_SECONDS_IN_YEAR < 1)
-                        System.err.printf ("*Warning*: timezone '%s', offset 
transition at %s would be a potential rounding error\n", timezone.getId (), 
transition.getInstant ());
+                for (int k = 0; k < transitions.size (); k++) {
+                    var  transition = transitions.get (k);
+                    var  instant    = transition.getInstant ();
+                    last_offset = addTransition (timezone, transition_data, 
instant, base, last_offset, transition.getOffsetAfter ());
+
+                    // Quite a few timezones have DST changes not covered by 
the exposed transitions (see
+                    // 'standardTransitions' in 'ZoneRules.java', no way to 
extract those without hacks).
+                    // Because of that, we'd sometimes use invalid DST/non-DST 
timezone name variant.  As a
+                    // workaround, we scan instants between subsequent 
"official" transitions with a day step
+                    // (should be more than enough) and check if DST changes.  
If yes, we use binary search to
+                    // finally pinpoint the exact instant of the change.
+                    Instant  limit;
+                    if (k + 1 < transitions.size ())
+                        limit = transitions.get (k + 1).getInstant ();
+                    else if (rules.getTransitionRules ().isEmpty ())
+                        limit = instant.plusSeconds (86400 * 365 * 25);
+                    else
+                        continue;
+
+                    while (true) {
+                        var  next = instant.plusSeconds (86400);
+                        if (!next.isBefore (limit))
+                            break;
+
+                        if (!Objects.equals (Offset.at (rules, next), 
last_offset)) {
+                            var  before = instant;
+                            var  after  = next;
 
-                    while (year_offset >= transition_data.size ())
-                        transition_data.add (new ArrayList <> (Arrays.asList 
(last_offset)));
+                            while (after.getEpochSecond () - 
before.getEpochSecond () > 1) {
+                                var  middle = Instant.ofEpochSecond 
((after.getEpochSecond () + before.getEpochSecond ()) / 2);
+                                if (Objects.equals (Offset.at (rules, middle), 
last_offset))
+                                    before = middle;
+                                else
+                                    after = middle;
+                            }
 
-                    transition_data.get (year_offset).add 
(transition.getInstant ().getEpochSecond () - (base + year_offset * 
AVERAGE_SECONDS_IN_YEAR));
-                    last_offset = transition.getOffsetAfter ().getTotalSeconds 
();
+                            last_offset = addTransition (timezone, 
transition_data, after, base, last_offset, rules.getOffset (next));
+                            next        = after;
+                        }
 
-                    // Floating-point offset is our internal mark of a 
transition to DST.
-                    // Java is over-eager to convert ints to float for us, so 
we format
-                    // them as strings manually now and add '.0' if 
appropriate.
-                    boolean  to_dst = !Objects.equals 
(transition.getOffsetAfter (), rules.getStandardOffset (transition.getInstant 
()));
-                    transition_data.get (year_offset).add (String.format 
(to_dst ? "%d.0" : "%d", last_offset));
+                        instant = next;
+                    }
                 }
 
                 List <Object>  transition_rule_data = new ArrayList <> ();
@@ -374,6 +400,22 @@ public class HarvestData
         System.out.println (")");
     }
 
+    protected static Offset addTransition (ZoneId timezone, List <List 
<Object>> transition_data, Instant instant, long base, Offset last_offset, 
ZoneOffset new_offset)
+    {
+        int  year_offset = (int) ((instant.getEpochSecond () - base) / 
AVERAGE_SECONDS_IN_YEAR);
+        if ((instant.getEpochSecond () + 1 - base) % AVERAGE_SECONDS_IN_YEAR < 
1)
+            System.err.printf ("*Warning*: timezone '%s', offset transition at 
%s would be a potential rounding error\n", timezone.getId (), instant);
+
+        while (year_offset >= transition_data.size ())
+            transition_data.add (new ArrayList <> (List.of (last_offset.toLisp 
())));
+
+        transition_data.get (year_offset).add (instant.getEpochSecond () - 
(base + year_offset * AVERAGE_SECONDS_IN_YEAR));
+
+        last_offset = new Offset (new_offset.getTotalSeconds (), 
timezone.getRules ().isDaylightSavings (instant));
+        transition_data.get (year_offset).add (last_offset.toLisp ());
+        return last_offset;
+    }
+
 
     protected static void printTimezoneNameData () throws Exception
     {
@@ -574,4 +616,21 @@ public class HarvestData
     {
         return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
     }
+
+
+    protected static record Offset (int seconds, boolean dst)
+    {
+        public static Offset at (ZoneRules rules, Instant instant)
+        {
+            return new Offset (rules.getOffset (instant).getTotalSeconds (), 
rules.isDaylightSavings (instant));
+        }
+
+        // Floating-point offset is our internal Lisp-level mark of a 
transition to DST.
+        // Java is over-eager to convert ints to float for us, so we format 
them as
+        // strings manually now and add '.0' if appropriate.
+        public Object toLisp ()
+        {
+            return String.format (dst ? "%d.0" : "%d", seconds);
+        }
+    }
 }
diff --git a/test/format.el b/test/format.el
index 40d79f8d9f..b8af6663d4 100644
--- a/test/format.el
+++ b/test/format.el
@@ -115,10 +115,32 @@
     (datetime--test-formatter-around-transition 1412438400)))
 
 (ert-deftest datetime-formatting-with-timezone-name-1 ()
-  (datetime--test-set-up-formatter 'Europe/Berlin 'en "yyyy-MM-dd HH:mm:ss z"
-    ;; Rule-based transition on 2014-10-26.  Should also result in
-    ;; timezone name changing between CEST and CET.
-    (datetime--test-formatter-around-transition 1414285200)))
+  (dolist (timezone (datetime-list-timezones))
+    (datetime--test-set-up-formatter timezone 'en "yyyy-MM-dd HH:mm:ss z"
+      ;; Rule-based transition on 2014-10-26.  In some timezones should also 
result in name
+      ;; changing, e.g. between CEST and CET.
+      (datetime--test-formatter-around-transition 1414285200))))
+
+(ert-deftest datetime-formatting-with-timezone-name-2 ()
+  ;; Many timezones had special relations with DST (see comments in 
'HarvestData.java'), so
+  ;; resulting name varies a lot.  Make sure we handle all that correctly.  
Too much to test all
+  ;; timezones, only some selected.
+  (dolist (timezone '(Africa/Algiers Africa/Tripoli Africa/Windhoek
+                      America/Anchorage America/Argentina/Buenos_Aires 
America/Argentina/Ushuaia America/Chihuahua America/Dawson America/Indiana/Knox 
America/Iqaluit America/Kentucky/Louisville America/Whitehorse
+                      Antarctica/McMurdo Antarctica/Palmer
+                      Asia/Almaty Asia/Ashkhabad Asia/Baku Asia/Bishkek 
Asia/Damascus Asia/Istanbul Asia/Kamchatka Asia/Omsk Asia/Singapore 
Asia/Tashkent Asia/Tbilisi Asia/Vladivostok Asia/Yerevan
+                      Atlantic/Azores
+                      Canada/Yukon
+                      Chile/EasterIsland
+                      Europe/Belfast Europe/Kaliningrad Europe/Lisbon 
Europe/Minsk Europe/Moscow Europe/Simferopol Europe/Tallinn Europe/Warsaw
+                      GB Libya NZ
+                      Pacific/Auckland Pacific/Norfolk
+                      Poland Portugal
+                      US/Alaska US/Aleutian US/Pacific
+                      W-SU))
+    (datetime--test-set-up-formatter timezone 'en "yyyy-MM-dd HH:mm:ss z"
+      ;; Exact numbers don't matter much, we just need to skip a few months 
each time.
+      (datetime--test-formatter (mapcar (lambda (k) (* k 7000000)) 
(number-sequence -300 400))))))
 
 (ert-deftest datetime-formatting-day-periods ()
   (let (times)
diff --git a/timezone-data.extmap b/timezone-data.extmap
index bf074b459f..731fd92781 100644
Binary files a/timezone-data.extmap and b/timezone-data.extmap differ

Reply via email to