https://gcc.gnu.org/g:020e02fcf281bd24065c8663219a22aba9d0a47d

commit r17-2466-g020e02fcf281bd24065c8663219a22aba9d0a47d
Author: Tomasz Kamiński <[email protected]>
Date:   Tue Jul 14 16:06:36 2026 +0200

    libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]
    
    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.
    
    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                     | 221 ++++++++++++++++-----
 .../testsuite/std/time/time_zone/116110.cc         |   4 +-
 .../testsuite/std/time/time_zone/pr116110_named.cc |  74 +++++++
 3 files changed, 244 insertions(+), 55 deletions(-)

diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
index 4ecce9e214ec..e2d607dccb98 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 tapproximate system time at, and
+      // collect 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 comparison on save adjusted until values (if needed)
+    // Assume 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 26b9ba33c316..1ec01d359552 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 000000000000..b3bf4eb1aa92
--- /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" );
+}

Reply via email to