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.

To 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 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.
        * libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc:
        (test_negative): Adjust test to avoid zone merge.
        (test_next_year): Test that 1945 Pacific/Auckland transition
        is properly handled.
        * testsuite/std/time/time_zone/zone_merge.cc: New test.

Co-authored-by: Álvaro Begué <[email protected]>
Signed-off-by: Álvaro Begué <[email protected]>
Signed-off-by: Tomasz Kamiński <[email protected]>
---
v3:
 * removes the reseeding of zone after resumption of expansion, fixed
   by https://gcc.gnu.org/pipermail/libstdc++/2026-July/067255.html
 * changes the implementation to follow my prototype using merge_window
   concept
 * apply merge_window on resumption, to avoid emmitting uncessary
   1h spans. They were more problematic compared to larger unmerged
   zone, as they presence would depend when expansion stopped, and
   on previous queries. 
 * update wall_cascade test I have added previously
To be able to detect jump_window, we need to know save value
at the end of the boundary. This is why in previous patch I have always
computed it. I could made a more ganular logic, but I do not think extra
complexity would help.

Will this patch (and all already merged) we are now producing same
zone info as Howard date, while we still do not merge neighbooring
same zones. There are 558 cases of such remaining, reduced from 753,
but not new cases are introduced.

Testing on x86_64-linux. *time_zone* test all passed in all standard
modes. OK for trunk?

This change is strickly depended on populating the save value at
the zone_boundary, for which I need to do some performance measurments
before considering it for backport.


 libstdc++-v3/src/c++20/tzdb.cc                | 70 +++++++++-------
 .../std/time/time_zone/wall_cascade.cc        | 41 ++++++---
 .../std/time/time_zone/zone_merge.cc          | 84 +++++++++++++++++++
 3 files changed, 152 insertions(+), 43 deletions(-)
 create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc

diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
index 43f6a0dbb84..22280e47a08 100644
--- a/libstdc++-v3/src/c++20/tzdb.cc
+++ b/libstdc++-v3/src/c++20/tzdb.cc
@@ -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;
       }
 
+    // Fo transitions, that leads to backward jump in the local time,
+    // and window of duplicated local time, the rule transition occuring
+    // during that window are considered to apply immediatelly at  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();
diff --git a/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc 
b/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
index f9167b79612..fcce1937ea6 100644
--- a/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
+++ b/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
@@ -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
index 00000000000..75942f976b6
--- /dev/null
+++ b/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc
@@ -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" );
+}
-- 
2.55.0

Reply via email to