https://gcc.gnu.org/g:ce236336c8416fcdb47975dfebbeb5d9a40c609d
commit r17-2553-gce236336c8416fcdb47975dfebbeb5d9a40c609d Author: Tomasz Kamiński <[email protected]> Date: Mon Jul 20 09:15:28 2026 +0200 libstdc++: Implement zic writezone merge optimization [PR124854] 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. This patch 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 rule 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. * testsuite/std/time/time_zone/wall_cascade.cc (test_next_year): Test that 1945 Pacific/Auckland transition is properly handled. (test_negative): Adjust test to avoid zone merge. * testsuite/std/time/time_zone/zone_merge.cc: New test. Reviewed-by: Jonathan Wakely <[email protected]> Co-authored-by: Álvaro Begué <[email protected]> Signed-off-by: Álvaro Begué <[email protected]> Signed-off-by: Tomasz Kamiński <[email protected]> Diff: --- libstdc++-v3/src/c++20/tzdb.cc | 70 +++++++++--------- .../testsuite/std/time/time_zone/wall_cascade.cc | 41 ++++++++--- .../testsuite/std/time/time_zone/zone_merge.cc | 84 ++++++++++++++++++++++ 3 files changed, 152 insertions(+), 43 deletions(-) diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc index 43f6a0dbb846..6fe3c6cfee64 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; } + // For transitions, that leads to backward jump in the local time, + // and window of duplicated local time, the rule transition occurring + // during that window are considered to apply immediately at 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 f9167b79612d..fcce1937ea62 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 000000000000..75942f976b6e --- /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" ); +}
