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.

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