[PATCH v2 3/5] libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]
Tomasz Kaminski
tkaminsk@redhat.com
Tue May 12 08:56:28 GMT 2026
On Mon, Apr 27, 2026 at 1:55 AM Álvaro Begué <alvaro.begue@gmail.com> 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 a fixup pass run after
> ranges::stable_sort(node->rules) at the end of reload_tzdb.
>
> A new ZoneInfo::m_until_save_pending bit (stolen from m_pos:15, which
> becomes m_pos:14 -- still 16384 max, far above any realistic offset
> into m_buf) marks pending entries. The parser sets this bit when
> it sees a wall-time UNTIL on a named-rule line and skips the save
> subtraction. The fixup pass walks every zone's ZoneInfos, looks up
> the active rule, and applies the deferred adjustment.
>
> The active-rule lookup uses a new helper find_pre_until_rule() that
> walks all (rule, year) pairs in chronological order and maintains a
> running save value, so wall-time rules' TIME fields are interpreted
> relative to the cascaded save state. The boundary it compares
> against shrinks as the running save grows, which gives zic.c's
> interpretation: a rule firing at exactly the wall UNTIL belongs to
> the next zone line, not the current one.
>
> The lazy-expansion seeding code that finds the active rule at
> info.begin is also updated to use a half-open `rule_start < t`
> window with `t = info.begin + 1s`, so a rule firing at exactly
> info.begin (the new line's first instant) is correctly identified
> as in force. Without this, the new line would seed with the wrong
> save and the first sys_info would have the wrong total offset and
> abbreviation.
>
> 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): Add m_until_save_pending bit
> (stolen from m_pos:15) and accessors until_save_pending(),
> set_until_save_pending(), clear_until_save_pending(), and
> adjust_until().
> (find_pre_until_rule): New function. Chronological cascade
> walker with iterative-boundary semantics, used by the post-
> parse fixup pass.
> (operator>>(istream&, ZoneInfo&)): Set m_until_save_pending
> when the wall UNTIL on a named-rule line cannot have its save
> subtracted at parse time. Replaces the FIXME.
> (time_zone::_M_get_sys_info): Change the seeding active-
> rule lookup to use t = info.begin + 1s, so a rule firing at
> exactly info.begin is included.
> (reload_tzdb): After sorting node->rules, run a fixup pass over
> every ZoneInfo with until_save_pending set, calling
> find_pre_until_rule and adjust_until.
> * testsuite/std/time/time_zone/116110.cc (test_apia): Remove
> the +11h workaround for the unfixed named-rule UNTIL bug; the
> canonical +10h boundary is now produced.
> * testsuite/std/time/time_zone/pr116110_named.cc: New test.
>
> Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
> ---
> libstdc++-v3/src/c++20/tzdb.cc | 122 +++++++++++++++++-
> .../testsuite/std/time/time_zone/116110.cc | 5 +-
> .../std/time/time_zone/pr116110_named.cc | 74 +++++++++++
> 3 files changed, 196 insertions(+), 5 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 c0d62bc35..648b9f85a 100644
> --- a/libstdc++-v3/src/c++20/tzdb.cc
> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> @@ -518,6 +518,20 @@ namespace std::chrono
> sys_seconds
> until() const noexcept { return m_until; }
>
> + // True if this is a named-rule zone line whose wall-time UNTIL
> still
> + // needs its SAVE adjustment applied. See reload_tzdb for the
> fixup.
> + bool
> + until_save_pending() const noexcept { return m_until_save_pending; }
> +
> + void
> + set_until_save_pending() noexcept { m_until_save_pending = 1; }
> +
> + void
> + clear_until_save_pending() noexcept { m_until_save_pending = 0; }
> +
> + void
> + adjust_until(seconds s) noexcept { m_until -= s; }
> +
> friend istream& operator>>(istream&, ZoneInfo&);
>
> bool
> @@ -571,8 +585,9 @@ 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_pos : 14 = 0; // offset of format() in m_buf
> uint_least16_t m_expanded : 1 = 0;
> + uint_least16_t m_until_save_pending : 1 = 0;
> duration<int_least16_t, ratio<60>> m_save{};
> sec32_t m_offset{};
> sys_seconds m_until{};
> @@ -679,6 +694,81 @@ namespace std::chrono
> }
> #endif
> };
> +
> + // Find the Rule whose save value is in force at the wall-time UNTIL
> + // of a Zone line, given that `wall_minus_stdoff` is the line's UNTIL
> + // with STDOFF subtracted and `stdoff` is the line's standard offset.
> + //
> + // Walks (rule, year) pairs chronologically, maintaining a running
> + // save value used to interpret subsequent Wall-indicator rules.
> + // The boundary `wall_minus_stdoff - running_save` shrinks as save
> + // accumulates, so a rule firing AT the boundary is treated as
> + // belonging to the next zone line.
> + //
> + // The calendar window extends by one year on each side to catch
> + // rules whose wall at_time crosses a year boundary in UT due to a
> + // large stdoff or save.
> + template<typename _RuleRange>
>
Replace rule_range with span<const Rule>, and similary change the
declaration
of variable:
auto rules = ranges::equal_range(node->rules, ri.rules(),
To span<const Rule>. Span has constructor for any contigous range, so that
should just
compile.
> + const Rule*
> + find_pre_until_rule(const _RuleRange& rules,
> + sys_seconds wall_minus_stdoff, seconds stdoff)
> + {
> + if (rules.empty())
> + return nullptr;
> +
> + const year last_year
> + = year_month_day{chrono::floor<days>(wall_minus_stdoff)}.year()
> + + years(1);
> + year first_year = year::max();
> + for (const auto& r : rules)
> + if (r.from < first_year)
> + first_year = r.from;
> + if (first_year > last_year)
> + return nullptr;
> +
> + struct Pending
> + {
> + const Rule* rule;
> + year y;
> + sys_seconds approx_when;
> + };
> + vector<Pending> pending;
> + pending.reserve(64);
> + for (year y = first_year; y <= last_year; ++y)
> + for (const auto& r : rules)
> + {
> + if (y < r.from || y > r.to)
> + continue;
> + seconds approx_off{};
> + if (r.when.indicator == at_time::Wall
> + || r.when.indicator == at_time::Standard)
> + approx_off = stdoff;
> + pending.push_back({&r, y, r.start_time(y, approx_off)});
> + }
> + std::sort(pending.begin(), pending.end(),
> + [](const Pending& a, const Pending& b) {
> + return a.approx_when < b.approx_when;
> + });
> +
> + seconds running_save{};
> + sys_seconds boundary = wall_minus_stdoff;
> + const Rule* last_fired = nullptr;
> + for (const auto& p : pending)
> + {
> + seconds offset{};
> + if (p.rule->when.indicator == at_time::Wall)
> + offset = stdoff + running_save;
> + else if (p.rule->when.indicator == at_time::Standard)
> + offset = stdoff;
> + sys_seconds fire = p.rule->start_time(p.y, offset);
> + if (fire >= boundary)
> + continue;
> + last_fired = p.rule;
> + running_save = p.rule->save;
> + boundary = wall_minus_stdoff - running_save;
> + }
> + return last_fired;
> + }
> } // namespace
> #endif // TZDB_DISABLED
>
> @@ -867,7 +957,9 @@ namespace std::chrono
>
> if (letters.empty())
> {
> - sys_seconds t = info.begin - seconds(1);
> + // info.begin + 1s makes the strict `rule_start < t` search
> + // inclusive of a rule that fires at exactly info.begin.
> + sys_seconds t = info.begin + seconds(1);
> const year_month_day date(chrono::floor<days>(t));
>
> // Try to find a Rule active before this time, to get initial
> @@ -1625,6 +1717,27 @@ namespace std::chrono
> ranges::sort(node->db.links, {}, &time_zone_link::name);
> ranges::stable_sort(node->rules, {}, &Rule::name);
>
> + // For every Zone line whose UNTIL was a wall-time expression on a
> + // named-rule line, the parser deferred the SAVE adjustment because
> + // the active rule was not yet identifiable. Now that all Rule
> + // records are loaded and indexed, find the rule active just before
> + // the wall UNTIL and subtract its save from m_until.
> + for (const auto& tz : node->db.zones)
> + {
> + auto& infos = tz._M_impl->infos;
> + for (auto& info : infos)
> + {
> + if (!info.until_save_pending())
> + continue;
> + auto rules = ranges::equal_range(node->rules, info.rules(),
> + ranges::less{}, &Rule::name);
> + if (const Rule* r
> + = find_pre_until_rule(rules, info.until(), info.offset()))
> + info.adjust_until(seconds(r->save));
> + info.clear_until_save_pending();
> + }
> + }
> +
> return Node::_S_replace_head(std::move(head), std::move(node));
> #else
> __throw_disabled();
> @@ -2386,7 +2499,10 @@ namespace std::chrono
> {
> if (inf.m_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
> + // Named Rule: defer SAVE adjustment until reload_tzdb
> + // has loaded all Rule records.
> + inf.set_until_save_pending();
> }
> }
> }
> diff --git a/libstdc++-v3/testsuite/std/time/time_zone/116110.cc
> b/libstdc++-v3/testsuite/std/time/time_zone/116110.cc
> index 26b9ba33c..7827387b5 100644
> --- a/libstdc++-v3/testsuite/std/time/time_zone/116110.cc
> +++ b/libstdc++-v3/testsuite/std/time/time_zone/116110.cc
> @@ -65,8 +65,9 @@ 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 + save 1h
> + // = -10h), so the boundary is at local_days + 24h + 10h.
> + 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 000000000..b3bf4eb1a
> --- /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.34.1
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260512/0991a495/attachment-0001.htm>
More information about the Libstdc++
mailing list