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