On Wed, 29 Jul 2026 at 08:40, Tomasz Kaminski <[email protected]> wrote:
>
>
>
> On Mon, Jul 20, 2026 at 10:17 AM 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.
>>  * 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?
>
> OK to backport to 16 with 
> https://gcc.gnu.org/pipermail/libstdc++/2026-July/067367.html?

OK for gcc-16

>>
>>
>> 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