On Mon, Jul 20, 2026 at 11:01 AM Jonathan Wakely <[email protected]>
wrote:

> On Mon, 20 Jul 2026, 09:16 Tomasz Kamiński, <[email protected]> wrote:
> >
> > 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.
>
>
> Yes, those 1h spans are actual bugs where the info is wrong, which is
> worse than just having two unmerged infos with the same properties.
>
You are right for the situation when that happens between zone lines
from file (i.e. original zone ends), but when we resume expansion, we
assume we are in STD zone and pick up the last transition we applied
(put at in STD). So in this case we would emit 1h span with correct
information
followed up by rest of it.  I would not give incorrect time, but will be
non-deterministic.

But, it turned out there is great synergy between zic zone-line merging,
and fix we need for restart of expansion: just apply it always and fix both.


> >
> >  * 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.
>
>
> Excellent.
>
> >
> > Testing on x86_64-linux. *time_zone* test all passed in all standard
> > modes. OK for trunk?
>
>
> OK with a few comment typos (see below).
>
> >
> > 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,
>
> "For"
>
> >
> > +    // and window of duplicated local time, the rule transition occuring
>
> "occurring"
>
> > +    // during that window are considered to apply immediatelly at
> boundary.
>
> "immediately"
> "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();
> > 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