]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
libstdc++: Implement zic writezone merge optimization [PR124854]
authorTomasz Kamiński <tkaminsk@redhat.com>
Mon, 20 Jul 2026 07:15:28 +0000 (09:15 +0200)
committerTomasz Kamiński <tkaminsk@redhat.com>
Mon, 20 Jul 2026 09:39:50 +0000 (11:39 +0200)
When two adjacent Zone lines have different total offsets and the
new line's rule set has a rule firing within jump of the boundary
(where jump = old_total - new_total > 0, i.e. local time goes
backward at the boundary), zic folds that rule into the boundary
itself: the single transition emitted has the rule's save value
already applied, so the new line begins with the post-rule save
rather than briefly using the pre-rule save and then transitioning
again moments later.

Canonical examples handled by the new merge block:
* America/Argentina/Buenos_Aires 1999-10-03: lines change
  stdoff -3 → -4 with an Argentina DST rule firing on the same
  day. Without the merge, chrono emits a 1-hour stretch of
  offset=-4 save=0 and then transitions to offset=-3 save=1;
  with the merge, the boundary itself is at offset=-3 save=1.
* Europe/Berlin 1945-05-24: lines split a rule set, with the
  So 1945-May-24 rule (save=2, "CEMT") firing at 01:00 UTC in
  the new frame, inside the 1h backward window.

Similarly, when the zone expansion algorithm is resumed after DST
span (save = 1h), we will revisit the corresponding STD transition,
for example: given offset +2h, the transition happening at 12:00
local time, will be considered twice:
 * 09:00 UT (2h + 1h) - proper ending of DST span
 * 10:00 UT (2h) - after re-entry.
Previously this transition were rejected using the (now removed)
rule_start - t < days(1) check, preventing us from emitting the one
hour STD time range followed by rest of same range. In this patch,
we reuse same merge logic, as such STD transition happens during
backward jump from boundary introduced by previous expansion.

This patch address both of the above by computing length of the
backward jump (merge_window) at the start of the expansion (regardless
if initial or re-entry), and then considering initial rule transition
happening in [info.begin + merge_window] to apply at info.begin.

libstdc++-v3/ChangeLog:

PR libstdc++/124854
* src/c++20/tzdb.cc (ZoneInfo::save): Define.
(time_zone::_M_get_sys_info): Fold initial transition if they
occurs in duplicated local time window (merge_window) due move
from DST to STD zone.
* testsuite/std/time/time_zone/wall_cascade.cc (test_next_year):
Test that 1945 Pacific/Auckland transition is properly handled.
(test_negative): Adjust test to avoid zone merge.
* testsuite/std/time/time_zone/zone_merge.cc: New test.

Reviewed-by: Jonathan Wakely <jwakely@redhat.com>
Co-authored-by: Álvaro Begué <alvaro.begue@gmail.com>
Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
Signed-off-by: Tomasz Kamiński <tkaminsk@redhat.com>
libstdc++-v3/src/c++20/tzdb.cc
libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc [new file with mode: 0644]

index 43f6a0dbb846bf6c8c7f2a038ec3a5bf04e006b5..6fe3c6cfee64bcc55cf2f0a03e415a0034c980cf 100644 (file)
@@ -541,6 +541,11 @@ namespace std::chrono
       bool
       calc_save(span<const Rule> all_rules) noexcept;
 
+      // save value at the transition boundary, usable if expanded()
+      // is true or after calc_save() was called.
+      seconds
+      save() const noexcept { return m_save; }
+
       friend istream& operator>>(istream&, ZoneInfo&);
 
       bool
@@ -1122,8 +1127,7 @@ namespace std::chrono
     // This is true by construction, because this function always tries to
     // finish so that the last ZoneInfo object expanded is for daylight time.
     // This means that i[-1] is either an expanded ZoneInfo for a DST sys_info
-    // or is an unexpanded (rule-based) ZoneInfo for a different rule, and
-    // rule changes always occur between periods of standard time.
+    // or is an unexpanded (rule-based) ZoneInfo for a different rule.
     info.offset = ri.offset();
     info.save = 0min;
     info.end = ri.until();
@@ -1149,6 +1153,18 @@ namespace std::chrono
          letters = first_std->letters;
       }
 
+    // For transitions, that leads to backward jump in the local time,
+    // and window of duplicated local time, the rule transition occurring
+    // during that window are considered to apply immediately at the boundary.
+    // This window is [info.begin, info.begin + merge_window].
+    seconds merge_window(0);
+    if (i != infos.begin())
+      {
+       const auto prev_offset = i[-1].offset() + i[-1].save();
+       if (prev_offset > info.offset)
+         merge_window = prev_offset - info.offset;
+      }
+
     const Rule* curr_rule = nullptr;
 
     while (info.begin < info.end && num_after > 0)
@@ -1194,9 +1210,6 @@ namespace std::chrono
 
            if (t < rule_start && rule_start < info.end)
              {
-               if (rule_start - t < days(1)) // XXX shouldn't be needed!
-                 continue;
-
                // Found a closer transition than the previous info.end.
                info.end = rule_start;
                next_rule = &rule;
@@ -1205,41 +1218,34 @@ namespace std::chrono
 
        format_abbrev_str(info, letters);
 
-       bool merged = false;
-#if 0
-       if (!new_infos.empty())
-         {
-           auto& back = new_infos.back();
-           if (back.offset == info.offset && back.abbrev == info.abbrev
-                 && back.save == info.save)
-             {
-               // This is a continuation of the previous sys_info.
-               back.end = info.end;
-               merged = true;
-             }
-         }
-#endif
-
        if (next_rule)
          letters = next_rule->letters;
        else
          letters = {};
 
-       if (!merged)
-         new_infos.emplace_back(info, letters);
-
-       if (info.begin <= tp && tp < info.end) // Found the result.
-         result_index = new_infos.size() - 1;
-       else if (result_index >= 0 && !merged)
+       // Transitions occuring in the backward jump time window occuring
+       // on zone transitions should be folded into zone change.
+       if (info.end - t <= merge_window)
+         info.begin = t;
+       else
          {
-           // Finish before a STD sys_info if possible, so that if we resume
-           // generating sys_info objects after this time point, save=0
-           // should be correct for the next sys_info.
-           if (num_after > 1 || !next_rule || next_rule->save == 0s)
-             --num_after;
+           new_infos.emplace_back(info, letters);
+
+           if (info.begin <= tp && tp < info.end) // Found the result.
+             result_index = new_infos.size() - 1;
+           else if (result_index >= 0)
+             {
+               // Finish before a STD sys_info if possible, so that if we resume
+               // generating sys_info objects after this time point, save=0
+               // should be correct for the next sys_info.
+               if (num_after > 1 || !next_rule || next_rule->save == 0s)
+                 --num_after;
+             }
+
+           info.begin = info.end;
          }
+       merge_window = seconds(0);
 
-       info.begin = info.end;
        if (next_rule)
          {
            info.end = ri.until();
index f9167b79612d5d20fb074f619e544e23f1adf115..fcce1937ea626458eb134892205e62d82eb92f0a 100644 (file)
@@ -82,7 +82,7 @@ test_negative()
   std::ofstream("tzdata.zi") << R"(# version test_negative_cascade
 R Fr 1945 o - Apr 2  2 -3 M
 R Fr 1945 o - Sep 16 0 0 -
-Z Test/Negative 0  -  X     1945 Sep 16 1u
+Z Test/Negative -2  -  X     1945 Sep 16 1u
              1  Fr CE%sT
 )";
 
@@ -109,11 +109,9 @@ Z Test/Negative 0  -  X     1945 Sep 16 1u
   // Test the firing of Sep 16 rule
   auto at_sep_rule = tz->get_info(sys_seconds{
     sys_days(1945y/September/16) + 2h});
-  // The transition_window < 1d condition triggers, and
-  // transition is ignored.
-  // VERIFY( at_sep_rule.offset == 1h );
-  // VERIFY( at_sep_rule.save == 0h );
-  // VERIFY( at_sep_rule.abbrev == "CET" );
+  VERIFY( at_sep_rule.offset == 1h );
+  VERIFY( at_sep_rule.save == 0h );
+  VERIFY( at_sep_rule.abbrev == "CET" );
 }
 
 void
@@ -137,13 +135,34 @@ Z Pacific/AucklandUT 11:39:4 - LMT 1868 N 2
   VERIFY( override_used ); // If this fails then XFAIL for the target.
   VERIFY( db.version == "test_next_year" );
 
-  // Pacific/Auckland requires both PR124854 and PR116110 to work
-  // correctly. TODO test it once implemented.
-  // The UT version uses 1945-12-31 13:00:00 UT after
-  // the rule application change.
-  auto* utz = locate_zone("Pacific/AucklandUT");
+  // The time zone change happens at 1945-12-31 12:00:00 UT, as
+  // total offset is 11:30 + 0:30
+  auto* tz = locate_zone("Pacific/Auckland");
 
   // Before the change
+  auto before_boundary
+   = tz->get_info(sys_seconds{sys_days(1945y/December/31) + 12h - 1s});
+  VERIFY( before_boundary.offset == 12h );
+  VERIFY( before_boundary.save == 30min );
+  VERIFY( before_boundary.abbrev == "NZST" );
+
+  // The Jan 1 rule is immediatelly in effect
+  auto at_boundary
+    = tz->get_info(sys_seconds{sys_days(1945y/December/31) + 12h});
+  VERIFY( at_boundary.offset == 12h );
+  VERIFY( at_boundary.save == 0h );
+  VERIFY( at_boundary.abbrev == "NZST" );
+
+  auto after_boundary
+    = tz->get_info(sys_seconds{sys_days(1945y/December/31) + 13h});
+  VERIFY( after_boundary.offset == 12h );
+  VERIFY( after_boundary.save == 0h );
+
+  // The UT version uses 1945-12-31 13:00:00 UT after the rule
+  // Jan 1 1946 rule application.
+  auto* utz = locate_zone("Pacific/AucklandUT");
+
+  // Before the Jan 1 rule application
   auto before_utboundary
    = utz->get_info(sys_seconds{sys_days(1945y/December/31) + 11h});
   VERIFY( before_utboundary.offset == 12h );
diff --git a/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc b/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc
new file mode 100644 (file)
index 0000000..75942f9
--- /dev/null
@@ -0,0 +1,84 @@
+// { dg-do run { target c++20 } }
+// { dg-require-effective-target tzdb }
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
+// When two adjacent Zone lines differ in total offset and the new line's
+// rule set has a rule firing within |jump| of the boundary (where jump
+// is a backward local-time jump), zic.c's writezone folds that rule
+// into the boundary, so the new line begins with the post-rule save.
+//
+// Mirrors America/Argentina/Buenos_Aires around 1999-10-03.
+
+#include <chrono>
+#include <fstream>
+#include <testsuite_hooks.h>
+
+static bool override_used = false;
+
+namespace __gnu_cxx
+{
+  const char* zoneinfo_dir_override() {
+    override_used = true;
+    return "./";
+  }
+}
+
+int
+main()
+{
+  using namespace std::chrono;
+
+  // stdoff jumps from -3 to -4 at the same instant a save=1 rule fires.
+  // In the new (-4) frame the rule fires 1 hour after the boundary at
+  // UT 03:00, so the merge folds the rule into the boundary and the
+  // new line begins at offset=-3, save=1 (abbrev "-03").
+  std::ofstream("tzdata.zi") << R"(# version test_zone_merge
+R T 1999 o - O 3 0 1 -
+R T 2000 o - Mar 3 0 0 -
+Z Test/BA -3 -  %z  1999 O 3
+          -4 T  %z  2000 Mar 3
+          -3 -  %z
+)";
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used );
+  VERIFY( db.version == "test_zone_merge" );
+
+  auto* tz = locate_zone("Test/BA");
+
+  // The boundary is the wall UNTIL "1999 O 3" (default time 00:00)
+  // interpreted in the prior (-3) frame, i.e. UT 03:00 1999-10-03.
+  sys_seconds boundary{sys_days(1999y/October/3) + 3h};
+
+  auto before = tz->get_info(boundary - 1s);
+  VERIFY( before.offset == -3h );
+  VERIFY( before.save == 0min );
+  VERIFY( before.abbrev == "-03" );
+
+  // The new line's first sys_info already has save=1 from the merge,
+  // total offset -3h, abbrev "-03".
+  auto at_boundary = tz->get_info(boundary);
+  VERIFY( at_boundary.offset == -3h );
+  VERIFY( at_boundary.save == 60min );
+  VERIFY( at_boundary.abbrev == "-03" );
+
+  auto plus_30min = tz->get_info(boundary + 30min);
+  VERIFY( plus_30min.offset == -3h );
+  VERIFY( plus_30min.save == 60min );
+  VERIFY( plus_30min.abbrev == "-03" );
+
+  // Sanity: well after the boundary, still in the merged sys_info
+  // until the Mar 3 2000 transition.
+  auto winter = tz->get_info(sys_days(2000y/January/15));
+  VERIFY( winter.offset == -3h );
+  VERIFY( winter.save == 60min );
+  VERIFY( winter.abbrev == "-03" );
+
+  // After Mar 3 2000: line 2 ends, line 3 begins.  No DST rule fires
+  // at this boundary, so total offset reverts to -3h with save=0.
+  auto spring = tz->get_info(sys_days(2000y/April/15));
+  VERIFY( spring.offset == -3h );
+  VERIFY( spring.save == 0min );
+  VERIFY( spring.abbrev == "-03" );
+}