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