On Wed, Jul 15, 2026 at 10:45 AM Tomasz Kamiński <[email protected]> wrote:
> The previous patches for PR 116110 left one case unresolved: a Zone > line with a wall-time UNTIL whose RULES field is a named rule set. > The save value used to convert the wall UNTIL to UTC depends on which > rule of the set was active at the UNTIL instant, but at parse time > the rule records have not all been loaded, so the active-rule lookup > can't yet be performed. > > The remaining FIXME in operator>>(istream&, ZoneInfo&) caused zones > like Africa/Algiers (around 1977-10-21) to place their zone-line > boundary one save-period off from the canonical zic interpretation, > producing brief incorrect sys_info windows during DST transitions. > > This commit defers the save adjustment to _M_get_sys_info call, > and applies it only for the ZoneInfo surrounding the specified > time. This assumes that applying the save does not change relative > order of ZoneInfo (i.e. the until members are spaced more than save). > > The m_expanded member is replaced with four state _M_state setting: > Expanded, and three separte state used for rule based zones: > * SaveKnown - m_save contains save value at time m_until > * SavePending - m_save is defualted to zero, m_until is correct > * UntilPending - as above, but also m_until time need to adjusted > The parser set the UntilPending state when it sees a wall-time > UNTIL on a named-rule line, and SaveKnown if not UNTIL date is > specified. > > The fixup in _M_get_sys_info, is performed by calc_save function > on previous/current zone. If necessary (depending on m_state) this > function updates m_save and m_until members, and returns true > if the m_until was shifted in the process. In such case, we redo > the comparision of until() against tp, and move iterator to active > zone info if necessary. The save at boundary for previous/current > ZoneInfo is required to implement proper zone merging, so it is > always computed. > > The active-rule lookup either reuses the existing find_active_rule > overload accepting sys_seconds (if UNTIL is not affected by save), > or newly introduced overload accepting local_seconds (local time). > The local_seconds versions, follows the same logic for finding > rule transitions (extracted to find_surrounding_transitions) > surrounding the sys_time. The final active rule, is then determined > after converting the transition times to local time, including the > running save. > > The test_apia case in 116110.cc had a hardcoded `+11h` workaround > for the unfixed bug; with this fix in place the workaround is removed > and the value becomes the canonical `+10h`. > > libstdc++-v3/ChangeLog: > > PR libstdc++/116110 > * src/c++20/tzdb.cc (ZoneInfo::calc_save, ZoneInfo::State): Define. > (ZoneInfo::m_expanded): Replaced with m_state. > (ZoneInfo::m_pos): Reduce the bit with to 14. > (ZoneInfo::m_state): Expands m_expanded with four state enum. > (ZoneInfo::ZoneInfo, ZoneInfo::expanded, ZoneInfo::set_abbrev): > Replace m_expanded usage with m_state and State::Expanded. > (Transitions, find_surrounding_transitions): Extracted from > find_active_rule. > (find_active_rule): Define overload for local_seconds. Implement > both in terms of find_surrounding_transitions. > (time_zone::_M_get_sys_info): Use ZoneInfo::calc_save to adjust > until() on surrounding zones, and shift the current zone iterator > (i) accordingly. > (operator>>(istream&, ZoneInfo&)): Set m_state for UntilPending > for UNTIL time using wall time, and SaveKnown for lines without > end date. For clarity, set m_save to 0 if daylight is not used. > * testsuite/std/time/time_zone/116110.cc (test_apia): Use correct > value of the the total offset. > testsuite/std/time/time_zone/pr116110_named.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 rewrites the implementation. The major change is the fact that > > save-adjustment is now applied lazily and locally to the neighbooring > zones. This have strong assumtion about zone changes being resonably > spaced (i.e. they remain sorted after save adjustment). After considering > that I believe this is resonable, as we are discussing save, and not > offset (that may jump for 24h). > This is now extracted to separate patch that should land before: https://gcc.gnu.org/pipermail/libstdc++/2026-July/067215.html > > The original v3 also changed the code to find active rule at time of > transition, to include transition happening exaclty at info begin. > I have dropped it for this version of the patch, as it was not > necessary to pass included test, and caused another failures. > We should revisit the comparision against the zic result once all > fixes are in place. > > Testing on x86_64-linux. All *time_zone* test passes. > We should also see the performance impact before any decision to > backport. > > > libstdc++-v3/src/c++20/tzdb.cc | 221 +++++++++++++----- > .../testsuite/std/time/time_zone/116110.cc | 4 +- > .../std/time/time_zone/pr116110_named.cc | 74 ++++++ > 3 files changed, 244 insertions(+), 55 deletions(-) > create mode 100644 > libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc > > diff --git a/libstdc++-v3/src/c++20/tzdb.cc > b/libstdc++-v3/src/c++20/tzdb.cc > index 998a363151d..09c2127b242 100644 > --- a/libstdc++-v3/src/c++20/tzdb.cc > +++ b/libstdc++-v3/src/c++20/tzdb.cc > @@ -483,6 +483,8 @@ namespace std::chrono > select_std_or_dst_abbrev(info.abbrev, info.save); > } > > + struct Rule; > + > // A time zone information record. > // Zone NAME STDOFF RULES FORMAT [UNTIL] > // Zone Asia/Amman 2:00 Jordan EE%sT 2017 Oct 27 01:00 > @@ -492,12 +494,12 @@ namespace std::chrono > ZoneInfo() = default; > > ZoneInfo(sys_info&& info) > - : m_buf(std::move(info.abbrev)), m_expanded(true), > m_save(info.save), > + : m_buf(std::move(info.abbrev)), m_state(Expanded), > m_save(info.save), > m_offset(info.offset - seconds(info.save)), m_until(info.end) > { } > > ZoneInfo(const pair<sys_info, string_view>& info) > - : m_expanded(true), m_save(info.first.save), > + : m_state(Expanded), m_save(info.first.save), > m_offset(info.first.offset - seconds(info.first.save)), > m_until(info.first.end) > { > @@ -533,17 +535,23 @@ namespace std::chrono > sys_seconds > until() const noexcept { return m_until; } > > + // For non-expanded zone (using rules) computes the value > + // of 'save' at the time of transitions, caches it in m_save. > + // Returns true if until value was changed in the process. > + bool > + calc_save(span<const Rule> all_rules) noexcept; > + > friend istream& operator>>(istream&, ZoneInfo&); > > bool > expanded() const noexcept > - { return m_expanded; } > + { return m_state == Expanded; } > > // For an expanded ZoneInfo this returns the LETTERS that apply to > the > // **next** sys_info after this one. > string_view > next_letters() const noexcept > - { return m_expanded ? rules() : string_view{}; } > + { return (m_state == Expanded) ? rules() : string_view{}; } > > > bool > @@ -551,7 +559,7 @@ namespace std::chrono > { > // If this object references a named Rule then we can't populate > // a sys_info yet. > - if (!m_expanded) > + if (m_state != Expanded) > return false; > > info.end = until(); > @@ -565,12 +573,16 @@ namespace std::chrono > private: > friend class time_zone; > > + enum class State : uint_least16_t > + { Expanded, SaveKnown, SavePending, UntilPending }; > + using enum State; > + > void > set_abbrev(string abbrev) > { > m_buf = std::move(abbrev); > m_pos = 0; > - m_expanded = true; > + m_state = Expanded; > } > > void > @@ -586,8 +598,8 @@ namespace std::chrono > } > > string m_buf; // rules() + ' ' + format() OR letters + ' ' + > format() > - uint_least16_t m_pos : 15 = 0; // offset of format() in m_buf > - uint_least16_t m_expanded : 1 = 0; > + uint_least16_t m_pos : 14 = 0; // offset of format() in m_buf > + State m_state : 2 = SavePending; > duration<int_least16_t, ratio<60>> m_save{}; > sec32_t m_offset{}; > sys_seconds m_until{}; > @@ -695,14 +707,27 @@ namespace std::chrono > #endif > }; > > - const Rule* > - find_active_rule(span<const Rule> rules, sys_seconds t, seconds > std_offset) > + struct Transitions > { > - struct Transition { > + struct Entry > + { > const Rule* rule; > sys_seconds when; > }; > > + Entry prev{nullptr, sys_seconds::min()}; > + Entry curr{nullptr, sys_seconds::min()}; > + Entry next{nullptr, sys_seconds::max()}; > + }; > + > + // Collect transitions of rules surrounding specified time t: > + // * .curr - rule active directly before or at t, > + // * .prev - rule transition before trans.curr > + // * .next - rule transition directly after t > + Transitions > + find_surrounding_transitions(span<const Rule> rules, sys_seconds t, > + seconds std_offset) > + { > const year_month_day date(chrono::floor<days>(t)); > // Expand the search window for a time near the end > // of the year which can be pushed to next/previous > @@ -716,16 +741,7 @@ namespace std::chrono > if (date.month() == January && date.day() == day(1)) > --first_year; > > - // Rule specifying start time as Wall time, should apply > - // running 'save' accumulated by earlier rules. To handle > - // that we firstly collect transitions surrounding specified > - // time t, ignoring the 'save': > - // * curr_tran - rule active directly before or at t, > - // * prev_tran - rule transition before curr_tran > - // * next_tran - rule transition directly after t > - Transition prev_tran{nullptr, sys_seconds::min()}; > - Transition curr_tran{nullptr, sys_seconds::min()}; > - Transition next_tran{nullptr, sys_seconds::max()}; > + Transitions trans; > for (const auto& rule : rules) > { > if (last_year < rule.from) // Rule doesn't apply yet at time t. > @@ -761,7 +777,7 @@ namespace std::chrono > > if (first_year > rule.to) > // Rule no longer applies at time t, record last transition > - start_before = rule.start_time(rule.to, offset); > + start_before = rule.start_time(rule.to, offset); > else if (first_year == last_year) > for_year(first_year); > else > @@ -773,24 +789,35 @@ namespace std::chrono > for_year(last_year); > } > > - if (curr_tran.when < start_before) > + if (trans.curr.when < start_before) > { > - prev_tran = curr_tran; > - curr_tran = {&rule, start_before}; > + trans.prev = trans.curr; > + trans.curr = {&rule, start_before}; > } > - else if (prev_tran.when < start_before) > - prev_tran = {&rule, start_before}; > + else if (trans.prev.when < start_before) > + trans.prev = {&rule, start_before}; > > - if (start_after < next_tran.when) > - next_tran = {&rule, start_after}; > + if (start_after < trans.next.when) > + trans.next = {&rule, start_after}; > } > + return trans; > + } > + > + const Rule* > + find_active_rule(span<const Rule> rules, sys_seconds t, seconds > std_offset) > + { > + // Rule specifying start time as Wall time, should apply > + // running 'save' accumulated by earlier rules. To handle > + // that we firstly collect transitions surrounding specified > + // time t: > + auto trans = find_surrounding_transitions(rules, t, std_offset); > > // No rule was active at the time of t, running 'save' > // cannot change this output, as we have no save to apply. > - if (!curr_tran.rule) > + if (!trans.curr.rule) > return nullptr; > > - auto cascade_save = [](const Rule* from, Transition& to) > + auto cascade_save = [](const Rule* from, Transitions::Entry& to) > { > if (!from || from->save == seconds(0)) > return false; > @@ -800,19 +827,49 @@ namespace std::chrono > return true; > }; > > - if (cascade_save(curr_tran.rule, next_tran)) > - // Running save moved what we considered next_tran to time > - // before or at t, in that case next_tran is active rule. > - if (next_tran.when <= t) > - return next_tran.rule; > + if (cascade_save(trans.curr.rule, trans.next)) > + // Running save moved what we considered trans.next to time > + // before or at t, in that case trans.next is active rule. > + if (trans.next.when <= t) > + return trans.next.rule; > > - if (cascade_save(prev_tran.rule, curr_tran)) > - // Running save moved what we consider curr_tran to > - // time after t, in that case prev_tran is active rule. > - if (curr_tran.when > t) > - return prev_tran.rule; > + if (cascade_save(trans.prev.rule, trans.curr)) > + // Running save moved what we consider trans.curr to > + // time after t, in that case trans.prev is active rule. > + if (trans.curr.when > t) > + return trans.prev.rule; > > - return curr_tran.rule; > + return trans.curr.rule; > + } > + > + const Rule* > + find_active_rule(span<const Rule> rules, local_seconds t, seconds > std_offset) > + { > + // UNTIL or START time in local time should take a 'save' from > + // active rule. To handle approximate system time at, and > + // collection transitions surrounding it: > + const sys_seconds at(t.time_since_epoch() - std_offset); > + const auto trans = find_surrounding_transitions(rules, at, > std_offset); > + > + auto to_local = [&](const Transitions::Entry& tran, const Rule* > before) > + { > + local_seconds ls(tran.when.time_since_epoch() + std_offset); > + if (!before || !tran.rule) > + return ls; > + if (tran.rule->when.indicator == at_time::Wall) > + // 'when' was converted from local time without considering > + // running 'save' in first place > + return ls; > + return ls + before->save; > + }; > + > + // Translate the transitions time to local time, taking 'save' > + // into consideration, and recheck active rule. > + if (to_local(trans.next, trans.curr.rule) <= t) > + return trans.next.rule; > + if (to_local(trans.curr, trans.prev.rule) > t) > + return trans.prev.rule; > + return trans.curr.rule; > } > > const Rule* > @@ -835,6 +892,43 @@ namespace std::chrono > } > return first; > } > + > + bool > + ZoneInfo::calc_save(span<const Rule> all_rules) noexcept > + { > + // Expanded rule, or the save value was already computed. > + if (m_state < SavePending) > + return false; > + > + // Find the rules named by rules() > + span<const Rule> sel_rules > + = ranges::equal_range(all_rules, rules(), ranges::less{}, > &Rule::name); > + if (sel_rules.empty()) > + __throw_runtime_error("std::chrono::time_zone::get_info: invalid > data"); > + > + if (m_state == UntilPending) > + { > + // UNTIL was specified in Wall time, so it is affected by 'save'. > + // Convert it back to local time, and find active rule using > that. > + const local_seconds lu(m_until.time_since_epoch() + m_offset); > + if (const Rule* rule = find_active_rule(sel_rules, lu - > seconds(1), m_offset)) > + if (rule->save.count() != 0) > + { > + m_state = SaveKnown; > + m_save = duration_cast<minutes>(rule->save); > + m_until -= rule->save; > + return true; > + } > + } > + else > + // UNTIL was either specified in Universal time, or Standard > + // time (known total offset). m_until is corresponding sys_time > point. > + if (const Rule* rule = find_active_rule(sel_rules, m_until - > seconds(1), m_offset)) > + m_save = duration_cast<minutes>(rule->save); > + > + m_state = SaveKnown; > + return false; > + } > } // namespace > #endif // TZDB_DISABLED > > @@ -966,19 +1060,35 @@ namespace std::chrono > // Find the transition info for the time point. > auto i = ranges::upper_bound(infos, tp, ranges::less{}, > &ZoneInfo::until); > > + // Perform the comparision on save adjusted until values (if needed) > + // Assumme that applying the save will not change relative order of > + // ZoneInfo objects. > + if (i != infos.begin() && i[-1].calc_save(node->rules) && > (i[-1].until() > tp)) > + --i; > + else if (i != infos.end() && i->calc_save(node->rules) && (i->until() > <= tp)) > + ++i; > + > if (i == infos.end()) > { > if (infos.empty()) > __throw_runtime_error("std::chrono::time_zone::get_info: invalid > data"); > - tp = (--i)->until(); > - } > + (--i)->calc_save(node->rules); > + tp = i->until(); > + } > + else // Guarantee that i->until() is correct > + i->calc_save(node->rules); > + > > sys_info info; > > if (i == infos.begin()) > info.begin = sys_days(year::min()/January/1); > else > - info.begin = i[-1].until(); > + { > + ZoneInfo& prev = i[-1]; > + prev.calc_save(node->rules); > + info.begin = prev.until(); > + } > > if (i->to(info)) // We already know a sys_info for this time. > return info; > @@ -990,10 +1100,10 @@ namespace std::chrono > const ZoneInfo& ri = *i; > > // Find the rules named by ri.rules() > - auto rules = ranges::equal_range(node->rules, ri.rules(), > - ranges::less{}, &Rule::name); > + span<const Rule> rules = ranges::equal_range(node->rules, ri.rules(), > + ranges::less{}, > &Rule::name); > > - if (ranges::empty(rules)) > + if (rules.empty()) > __throw_runtime_error("std::chrono::time_zone::get_info: invalid > data"); > > vector<pair<sys_info, string_view>> new_infos; > @@ -1012,8 +1122,6 @@ namespace std::chrono > // rule changes always occur between periods of standard time. > info.offset = ri.offset(); > info.save = 0min; > - // XXX The ri.until() time point should be > - // "interpreted using the rules in effect just before the transition" > info.end = ri.until(); > info.abbrev = ri.format(); > > @@ -2612,6 +2720,7 @@ constinit tzdb_list::_Node::NumLeapSeconds > tzdb_list::_Node::num_leap_seconds; > if (rules == "-") > { > // Standard time always applies, no DST. > + inf.m_save = minutes(0); > } > else > { > @@ -2645,14 +2754,20 @@ constinit tzdb_list::_Node::NumLeapSeconds > tzdb_list::_Node::num_leap_seconds; > inf.m_until -= seconds(inf.m_offset); > if (t.indicator != at_time::Standard) > { > - if (inf.m_expanded) // Not a named Rule, SAVE is known > now. > + if (inf.expanded()) // Not a named Rule, SAVE is known > now. > inf.m_until -= inf.m_save; > - // else Named Rule, SAVE is unknown. FIXME: PR 116110 > + else // else Named Rule, SAVE is unknown, mark as pending > + inf.m_state = ZoneInfo::UntilPending; > } > } > } > else > - inf.m_until = sys_days(year::max()/December/31); > + { > + inf.m_until = sys_days(year::max()/December/31); > + // The line does not define UNTIL, so it is not affected by save > + if (!inf.expanded()) > + inf.m_state = ZoneInfo::SaveKnown; > + } > > in.clear(in.rdstate() & ios::eofbit); > in.exceptions(ex); > diff --git a/libstdc++-v3/testsuite/std/time/time_zone/116110.cc > b/libstdc++-v3/testsuite/std/time/time_zone/116110.cc > index 26b9ba33c31..1ec01d35955 100644 > --- a/libstdc++-v3/testsuite/std/time/time_zone/116110.cc > +++ b/libstdc++-v3/testsuite/std/time/time_zone/116110.cc > @@ -65,8 +65,8 @@ test_apia() > auto* tz = locate_zone("Pacific/Apia"); > local_seconds t = local_days(2011y/December/29) + 24h; > > - // FIXME: this should be + 10h but we do not account for DST yet, so + > 11h. > - sys_seconds ut(t.time_since_epoch() + 11h ); > + // The wall UNTIL is interpreted in the prior offset (-11h + 1h) > + sys_seconds ut(t.time_since_epoch() + 10h); > sys_info info; > info = tz->get_info(ut - 1s); > VERIFY( info.offset == (-11h + info.save) ); > diff --git a/libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc > b/libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc > new file mode 100644 > index 00000000000..b3bf4eb1aa9 > --- /dev/null > +++ b/libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc > @@ -0,0 +1,74 @@ > +// { 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* } } > + > +// Africa/Algiers 1977-10-21: a Zone line whose RULES references a > +// named Rule and whose UNTIL is a wall-time expression. The wall > +// UNTIL is interpreted using the SAVE value in force just before the > +// boundary (the May-6 rule's save=1, not the Oct-21 rule's save=0 > +// even though the Oct-21 rule fires at the same wall instant). > +// > +// Rule d 1977 May 6 0:00 wall save=1 > +// Rule d 1977 Oct 21 0:00 wall save=0 > +// Z A 0 d WE%sT 1977 O 21 > +// 1 d CE%sT > + > +#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; > + > + std::ofstream("tzdata.zi") << R"(# version test_pr116110_named > +R d 1977 o - May 6 0 1 S > +R d 1977 o - O 21 0 0 - > +Z Test/Algiers 0 d WE%sT 1977 O 21 > + 1 d CE%sT > +)"; > + > + const auto& db = reload_tzdb(); > + VERIFY( override_used ); > + VERIFY( db.version == "test_pr116110_named" ); > + > + auto* tz = locate_zone("Test/Algiers"); > + > + // Just before the boundary: still in the first Zone line under > + // the May-6 rule (save=1, WEST, total +1). > + auto pre = tz->get_info(sys_days{1977y/October/20} + 22h); > + VERIFY( pre.offset == 1h ); > + VERIFY( pre.save == 1h ); > + VERIFY( pre.abbrev == "WEST" ); > + > + // The boundary is Oct 20 23:00 UTC (= wall 00:00 - stdoff(0) - > save(1)). > + // At and after the boundary we are in the second line (CET). > + auto at = tz->get_info(sys_days{1977y/October/20} + 23h); > + VERIFY( at.offset == 1h ); // stdoff 1 + save 0 (CET, second line) > + VERIFY( at.save == 0min ); > + VERIFY( at.abbrev == "CET" ); > + > + // A second query inside the second line, well clear of the boundary. > + auto after = tz->get_info(sys_days{1977y/October/21} + 12h); > + VERIFY( after.offset == 1h ); > + VERIFY( after.save == 0min ); > + VERIFY( after.abbrev == "CET" ); > + > + // A query inside the [Oct 20 23:00, Oct 21 00:00] UTC window must be > + // in the second line, not in a leftover stretch from the first line. > + auto window = tz->get_info(sys_days{1977y/October/20} + 23h + 30min); > + VERIFY( window.offset == 1h ); > + VERIFY( window.abbrev == "CET" ); > +} > -- > 2.55.0 > >
