[PATCH v2 0/5] libstdc++: chrono tzdb correctness fixes
Álvaro Begué
alvaro.begue@gmail.com
Sun Apr 26 23:42:54 GMT 2026
This is v2 of the chrono tzdb correctness fix series. The actual fixes
are unchanged; v2 addresses Tomasz Kamiński's review comments, which
were all stylistic.
Changes since v1 (per Tomasz's feedback):
* Trim verbose "Regression test:" / past-bug-history prose from the
test files. Test comments now describe the behavior under test
rather than the historical bug.
* Trim redundant cross-reference comments and shorten the surviving
ones. "// PR 116110" breadcrumbs in code comments are gone -- the
PR number is captured in the commit message and the ChangeLog entry.
* Patch 2: rename parse_on_day_body -> parse_day_spec; use C++20
designated initializers and if-with-initializer for `on_day` and
`abbrev_month`; restructure the optional DAY/TIME parse so TIME is
only attempted when DAY parsing succeeds.
Each commit now carries a Signed-off-by trailer.
A range-diff of v1 vs v2 is included at the end of this cover letter.
Test plan (rerun against current master HEAD):
* libstdc++ stage1 build of GCC 16.0.1 trunk (b99e67e8a) succeeds.
* All 7 chrono tests pass (Wakely's existing 116110 + 124513 plus the
5 new ones added by this series).
* Brute-force comparison harness (628310 samples × 447 zones) reports
zero mismatches against libc localtime_r.
* Abbreviation-only sweep (188493 samples) reports zero offset and
zero abbrev diffs.
The series builds on Jonathan Wakely's recent PR116110 / PR124513
work (commits 663e5ade1, cddf4111c, fbc5d2b1a). Patch 3 in particular
resolves the "FIXME: PR116110" left in operator>>(istream&, ZoneInfo&)
for the named-rule wall-UNTIL case.
1. Fix numeric save offset on Zone lines [PR124851].
ZoneInfo::m_offset had inconsistent semantics: the parser path
stored stdoff alone, but the two sys_info-taking constructors
stored the total (stdoff + save). Normalize m_offset to stdoff
alone everywhere; to() adds save back when reconstructing.
2. Support ON-format DAY in Zone UNTIL field [PR124852].
The UNTIL parser only accepted a plain integer as the DAY,
silently misparsing tzdata.zi entries like Europe/Simferopol's
"1997 Mar lastSu 1u". Reuse the on_day machinery and the
parse_day_spec helper.
3. Resolve named-rule UNTIL save adjustment [PR116110].
The remaining FIXME in operator>>(istream&, ZoneInfo&) for
wall-time UNTILs on named-rule zone lines. At parse time the
active rule cannot be evaluated, so the parser leaves the SAVE
adjustment pending and a fixup pass in reload_tzdb walks every
pending ZoneInfo and applies the adjustment using a new
find_pre_until_rule helper with iterative-boundary cascade
semantics. Removes the +11h workaround from test_apia in
116110.cc.
4. Cascade wall-time saves in lazy expansion seeding [PR124853].
Replace the per-rule isolated active-rule lookup in
_M_get_sys_info with a chronological cascade walker that
maintains a running save and interprets each Wall-time rule's
at_time relative to the cascaded state (matching zic.c's
outzone()).
5. Implement zic writezone merge optimization [PR124854].
Two related fixes: (a) always seed info.offset/save from
find_active_rule (not just when letters is empty), so partial-
expansion re-entry sees the right state; (b) add the writezone
merge for backward jumps at zone-line boundaries.
Álvaro Begué (5):
libstdc++: Fix numeric save offset on Zone lines [PR124851]
libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]
libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]
libstdc++: Cascade wall-time saves in lazy expansion seeding
[PR124853]
libstdc++: Implement zic writezone merge optimization [PR124854]
libstdc++-v3/src/c++20/tzdb.cc | 378 ++++++++++++++----
.../testsuite/std/time/time_zone/116110.cc | 5 +-
.../std/time/time_zone/numeric_save.cc | 58 +++
.../std/time/time_zone/pr116110_named.cc | 74 ++++
.../std/time/time_zone/until_day_on.cc | 168 ++++++++
.../std/time/time_zone/wall_cascade.cc | 70 ++++
.../std/time/time_zone/zone_merge.cc | 84 ++++
7 files changed, 748 insertions(+), 89 deletions(-)
create mode 100644
libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
create mode 100644
libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc
create mode 100644
libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
create mode 100644
libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc
Range-diff against v1:
1: 7e77ac729 ! 1: 2136a9e20 libstdc++: Fix numeric save offset on Zone
lines [PR 124851]
@@
## Metadata ##
-Author: Alvaro Begue <alvaro.begue@gmail.com>
+Author: Álvaro Begué <alvaro.begue@gmail.com>
## Commit message ##
- libstdc++: Fix numeric save offset on Zone lines [PR 124851]
+ libstdc++: Fix numeric save offset on Zone lines [PR124851]
When a Zone line specifies a numeric value as its RULES field (the
constant DST save value for that zone line, e.g. Africa/Gaborone's
@@ Commit message
populating sys_info::offset.
* testsuite/std/time/time_zone/numeric_save.cc: New test.
+ Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
+
## libstdc++-v3/src/c++20/tzdb.cc ##
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
}
- // STDOFF: Seconds from UTC during standard time.
-+ // STDOFF: Seconds from UTC during standard time. Always the
-+ // standard offset only; the saved value (if any) is in m_save
and
-+ // is added back when reconstructing a sys_info via to().
++ // STDOFF: Seconds from UTC during standard time (without any
save).
seconds
offset() const noexcept { return m_offset; }
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
info.end = until();
- info.offset = offset();
-+ // m_offset is the standard offset only; add the saved value to
-+ // reconstruct the total offset. See ZoneInfo's m_offset comment.
+ info.offset = offset() + seconds(m_save);
info.save = minutes(m_save);
info.abbrev = format();
@@ libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc (new)
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
-+// Regression test: when a Zone line specifies a numeric value as its
-+// RULES field, that value is the constant DST save value for that
zone
-+// line. ZoneInfo::to() previously set sys_info::offset to the zone
-+// line's STDOFF only, ignoring the parsed save. Per
[time.zone.info.sys]
-+// sys_info::offset is the *total* UTC offset (stdoff + save), so any
-+// zone line with a non-zero numeric save reported the wrong offset.
-+//
-+// Mirrors Africa/Gaborone's tzdata, which uses
-+// 2 - CAT 1943 S 19 2
-+// 2 1 CAST 1944 Mar 19 2 <-- numeric "1" RULES, save = +1h
-+// 2 - CAT
-+// The middle line is what triggers the bug.
++// When a Zone line specifies a numeric value as its RULES field, that
++// value is the constant DST save value for that zone line. Per
++// [time.zone.info.sys] sys_info::offset is the total UTC offset
++// (stdoff + save).
+
+#include <chrono>
+#include <fstream>
2: 57b11cf6e ! 2: 60c6f5eef libstdc++: Support ON-format DAY in Zone
UNTIL field [PR 124852]
@@
## Metadata ##
-Author: Alvaro Begue <alvaro.begue@gmail.com>
+Author: Álvaro Begué <alvaro.begue@gmail.com>
## Commit message ##
- libstdc++: Support ON-format DAY in Zone UNTIL field [PR 124852]
+ libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]
The Zone-line UNTIL parser only accepted a plain day-of-month
integer
for the DAY field, while the tzdata.zi grammar accepts the same
ON-style
@@ Commit message
left d == 1 when the day token wasn't a digit, then went on to
parse the
remainder as the TIME field.
- Fix by reusing the existing parse_on_day_body() helper that already
+ Factor out the day-component parser from operator>>(istream&,
on_day&)
+ as parse_day_spec(), and reuse it for the UNTIL DAY field.
parse_day_spec
handles all three on_day forms (DayOfMonth, LastWeekday, LessEq /
- GreaterEq) for Rule lines. The MONTH-only and YEAR-only short
forms are
- still accepted because the DAY/TIME fields are optional and
default to
- day 1, time 00:00. The on_day struct's pin() method handles the
- year/month-relative resolution.
+ GreaterEq). The MONTH-only and YEAR-only short forms are still
accepted
+ because the DAY/TIME fields are optional and default to day 1,
time 00:00.
+ The on_day struct's pin() method handles the year/month-relative
+ resolution.
The DAY field is unambiguously distinguishable from a TIME field
that
could otherwise follow the MONTH directly: per zic's grammar, MONTH
- must be followed by DAY before any TIME is allowed. So we always
+ must be followed by DAY before any TIME is allowed. So we always
attempt to parse a DAY if any non-whitespace remains after the
MONTH.
libstdc++-v3/ChangeLog:
PR libstdc++/124852
- * src/c++20/tzdb.cc (parse_on_day_body): Factor out the
day-
- component parser from operator>>(istream&, on_day&) so it
can
- be reused.
- (operator>>(istream&, on_day&)): Use the new helper.
+ * src/c++20/tzdb.cc (parse_day_spec): New function,
factored
+ out of operator>>(istream&, on_day&).
+ (operator>>(istream&, on_day&)): Use parse_day_spec.
(operator>>(istream&, ZoneInfo&)): Replace the integer DAY
- parser with parse_on_day_body for the UNTIL field.
+ parser with parse_day_spec for the UNTIL field.
* testsuite/std/time/time_zone/until_day_on.cc: New test.
+ Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
+
## libstdc++-v3/src/c++20/tzdb.cc ##
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
}
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+ // Read the day-component of an on_day expression (everything
after the
+ // month). Three forms are accepted: a plain day-of-month number,
+ // "lastXxx" where Xxx is a weekday name (LastWeekday), or
"Xxx<=N" or
-+ // "Xxx>=N" (LessEq / GreaterEq). The caller is responsible for
setting
-+ // `on.month` before calling. On failure the function sets
failbit and
-+ // leaves `on` unchanged.
++ // "Xxx>=N" (LessEq / GreaterEq). On failure the function sets
failbit
++ // and leaves `on` unchanged.
+ istream&
-+ parse_on_day_body(istream& in, on_day& on)
++ parse_day_spec(istream& in, on_day& on)
{
- on_day on{};
- abbrev_month m{};
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+ abbrev_month m{};
+ in >> m;
+ on.month = static_cast<unsigned>(m.m);
-+ if (parse_on_day_body(in, on))
++ if (parse_day_spec(in, on))
+ to = on;
+ return in;
+ }
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
{
int sign = 1;
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+ in.exceptions(ios::goodbit); // Don't throw ios::failure if
YEAR absent.
if (int y = int(year::max()); in >> y)
{
- abbrev_month m{January};
+- abbrev_month m{January};
- int d = 1;
-+ on_day on{};
-+ on.kind = on_day::DayOfMonth;
-+ on.month = 1; // default January
-+ on.day_of_month = 1; // default day-of-month 1
++ on_day on{.kind = on_day::DayOfMonth, .month = 1, .day_of_month =
1};
at_time t{};
- // XXX DAY should support ON format, e.g. lastSun or Sun>=8
- in >> m >> d >> t;
- inf.m_until = sys_days(year(y)/m.m/day(d)) + seconds(t.time);
-+ if (in >> m)
++ if (abbrev_month m{January}; in >> m)
+ {
+ on.month = static_cast<unsigned>(m.m);
-+ // The DAY field is optional. Per the tzdata.zi grammar,
-+ // a MONTH followed by anything more is always followed by
-+ // a DAY (possibly followed by a TIME); MONTH directly
-+ // followed by TIME is not a valid form. So if there's
-+ // any non-whitespace before end of line, parse a DAY.
+ if (!ws(in).eof())
-+ parse_on_day_body(in, on);
++ if (parse_day_spec(in, on))
++ in >> t;
+ }
-+ in >> t;
+ year_month_day ymd = on.pin(year(y));
+ inf.m_until = sys_days(ymd) + seconds(t.time);
if (t.indicator != at_time::Universal)
@@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
-+// Regression test: the DAY portion of a Zone line's UNTIL field
accepts
-+// not only a numeric day-of-month but also "lastXxx" (last weekday in
-+// the month) and "Xxx<=N" / "Xxx>=N" forms, just like the ON field of
-+// a Rule line. Previously the UNTIL parser used `int d; in >> d;`
which
-+// silently failed on the non-numeric forms and defaulted d to 1,
placing
-+// any zone-line transition with such an UNTIL on the wrong calendar
day.
++// The DAY portion of a Zone line's UNTIL field accepts not only a
++// numeric day-of-month but also "lastXxx" (last weekday in the month)
++// and "Xxx<=N" / "Xxx>=N" forms, just like the ON field of a Rule
line.
+//
+// Real-world example: Europe/Simferopol has
+// 3 - MSK 1997 Mar lastSu 1u
-+// which must place the boundary on 1997-03-30 (the last Sunday of
March),
-+// not on 1997-03-01.
++// which places the boundary on 1997-03-30 (the last Sunday of March).
+
+#include <chrono>
+#include <fstream>
@@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
+ auto at = tz->get_info(boundary);
+ VERIFY( at.abbrev == "X" );
+
-+ // Critical regression check: a sample 15 days BEFORE the boundary
must
-+ // still be in the MSK line. The unfixed parser placed the
boundary on
-+ // March 1 because "lastSu" defaulted to day 1, and a March-15 query
-+ // landed in the X line instead.
++ // Check that the lastSu day is parsed correctly, and not defaulted
++ // to the 1st: a March 15 query must still be in the MSK line.
+ auto mid_march = tz->get_info(sys_days{1997y/March/15});
+ VERIFY( mid_march.abbrev == "MSK" );
+ VERIFY( mid_march.offset == 3h );
@@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
+ auto at = tz->get_info(boundary);
+ VERIFY( at.abbrev == "B" );
+
-+ // A June-1 query must still be in the A line (the unfixed parser
-+ // placed the boundary on June 1).
++ // Check that Sun>=8 is parsed correctly, and not defaulted to the
1st.
+ auto early = tz->get_info(sys_days{1990y/June/1});
+ VERIFY( early.abbrev == "A" );
+}
@@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
+{
+ using namespace std::chrono;
+
-+ // Sanity check: a UNTIL with only a year (no MONTH, no DAY, no
TIME)
-+ // must continue to default to January 1 00:00.
++ // MONTH, DAY and TIME default to January 1st 00:00 if not
specified.
+ std::ofstream("tzdata.zi") << R"(# version test_year_only
+Z Test/YearOnly 0 - A 1990
+ 0 - B
@@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)
+{
+ using namespace std::chrono;
+
-+ // Sanity check: UNTIL with only YEAR and MONTH (no DAY, no TIME)
-+ // must default DAY to 1 and TIME to 00:00.
++ // DAY and TIME default to the 1st 00:00 if not specified.
+ std::ofstream("tzdata.zi") << R"(# version test_year_month_only
+Z Test/YearMonth 0 - A 1990 Jul
+ 0 - B
3: 38e5ea518 ! 3: 5b20eac08 libstdc++: Resolve named-rule UNTIL save
adjustment [PR116110]
@@
## Metadata ##
-Author: Alvaro Begue <alvaro.begue@gmail.com>
+Author: Álvaro Begué <alvaro.begue@gmail.com>
## Commit message ##
libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]
@@ Commit message
(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::_Impl::_M_get_sys_info): Change the seeding
active-
+ (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
@@ Commit message
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 ##
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
sys_seconds
until() const noexcept { return m_until; }
-+ // PR 116110: When the parser sees a wall-time UNTIL on a
named-Rule
-+ // line, the SAVE component of the conversion can't be applied
yet
-+ // because the active Rule depends on the cumulative state of
the
-+ // (not-yet-loaded) rule set. The parser leaves m_until in the
-+ // "STDOFF subtracted but SAVE not yet subtracted" intermediate
-+ // state and sets this bit; reload_tzdb runs a fixup pass after
all
-+ // Rule records are loaded to subtract the SAVE.
++ // 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; }
+
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+ void
+ clear_until_save_pending() noexcept { m_until_save_pending = 0;
}
+
-+ // Subtract `s` from m_until. Used by the PR 116110 fixup pass.
+ void
+ adjust_until(seconds s) noexcept { m_until -= s; }
+
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- 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; // PR 116110, see
above
++ 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{};
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+
+ // 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
-+ // expressed in the "save=0" frame (i.e. the parsed wall UNTIL
with
-+ // the line's STDOFF subtracted) and `stdoff` is the line's
standard
-+ // offset.
-+ //
-+ // The function walks all (rule, year) pairs in chronological
order,
-+ // maintaining a running save value. Wall-time rules have their
TIME
-+ // field interpreted relative to the running save (since "wall"
means
-+ // local civil time = stdoff + save), so a rule's effective UT
firing
-+ // time depends on which prior rule was last in force. This
matches
-+ // zic.c's outzone() logic and is required for zone lines whose
-+ // rule set has rules whose at_time depends on cascading saves.
++ // with STDOFF subtracted and `stdoff` is the line's standard
offset.
+ //
-+ // The comparison `fire < boundary` shrinks `boundary` as the
running
-+ // save cascades up: a rule that fires AT the boundary (with the
-+ // cascaded save applied) is treated as belonging to the next zone
-+ // line, and its save is excluded from the running total.
++ // 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.
+ //
-+ // Canonical case: Africa/Algiers 1977-10-21. The "Algeria 1977
-+ // Oct 21" rule (save=0) fires at the same instant as the wall
UNTIL
-+ // of line 6 ("0 d WE%sT 1977 O 21"). The pre-rule save (1h, from
-+ // the May 6 rule) is what determines the boundary's UT placement,
-+ // not the Oct 21 rule's save=0.
-+ //
-+ // The calendar window is extended by one year on each side, to
-+ // catch rules whose wall-time at_time falls in early January or
-+ // late December but whose UT firing crosses a year boundary due
-+ // to a large stdoff or save.
++ // 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>
+ const Rule*
+ find_pre_until_rule(const _RuleRange& rules,
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
if (letters.empty())
{
- sys_seconds t = info.begin - seconds(1);
-+ // We want the rule whose effect is in force at info.begin --
-+ // including a rule that fires at exactly info.begin (its effect
-+ // has just begun and is active for the first sys_info we are
-+ // about to generate). The search below uses a strict
-+ // `rule_start < t` comparison, so pass info.begin + 1s to make
-+ // the half-open lookup (..., info.begin] inclusive of the
-+ // boundary instant. This is what makes named-rule zone lines
-+ // like Africa/Algiers (PR 116110) seed with the correct save:
-+ // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's
-+ // frame, which is exactly the new line's begin.
++ // 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));
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
ranges::sort(node->db.links, {}, &time_zone_link::name);
ranges::stable_sort(node->rules, {}, &Rule::name);
-+ // PR 116110 fixup pass. 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 wasn't yet identifiable.
Now
-+ // that all Rule records are loaded and indexed, walk every
pending
-+ // ZoneInfo, find the rule whose effect was in force just before
the
-+ // wall UNTIL, and subtract that rule's save from m_until.
-+ //
-+ // "Just before the wall UNTIL" matches zic.c's interpretation:
the
-+ // wall time of UNTIL is read in the frame in effect immediately
-+ // prior to the boundary, so the SAVE used is the value that the
-+ // most recent rule strictly before the UNTIL set. A rule firing
at
-+ // exactly the UNTIL is not yet in force at the moment the wall
-+ // time is being interpreted -- its effect belongs to the next
zone
-+ // line, not this one. find_pre_until_rule's iterative-boundary
-+ // walker implements this semantics.
++ // 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;
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
inf.m_until -= inf.m_save;
- // else Named Rule, SAVE is unknown. FIXME: PR 116110
+ else
-+ // Named Rule: SAVE depends on which rule of the set
-+ // was active at this instant, which can only be
-+ // determined once all Rule records are loaded. Mark
-+ // the ZoneInfo so that the fixup pass in reload_tzdb
-+ // applies the deferred adjustment. PR 116110.
++ // Named Rule: defer SAVE adjustment until reload_tzdb
++ // has loaded all Rule records.
+ inf.set_until_save_pending();
}
}
@@ libstdc++-v3/testsuite/std/time/time_zone/116110.cc: test_apia()
- // 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 "2011 Dec 29 24" is interpreted in the prior
offset
-+ // (-11h + save 1h = -10h), so the boundary is at local_days +24h
+10h
-+ // (the FIXME for the +11h compensation has been resolved by the
-+ // fix for the named-rule UNTIL case in PR 116110).
++ // 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);
@@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
-+// Regression test for PR 116110, named-rule case.
++// 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).
+//
-+// A Zone line whose RULES references a named Rule and whose UNTIL is
a
-+// wall-time expression cannot have its UNTIL converted to a true UTC
-+// instant at parse time, because the SAVE value at the UNTIL depends
on
-+// which rule of the named set was last in force just before that wall
-+// time -- and that's not known until all Rule records have been
loaded
-+// and indexed. The "Partial fix for interpretation of non-UTC UNTIL
-+// times" commit handled the simpler cases (UNTIL with `s` indicator,
-+// `u` indicator, or wall + non-named RULES) but explicitly left a
-+// FIXME for the named-rule case. This test exercises that case via
-+// the canonical Africa/Algiers boundary at 1977-10-21.
-+//
-+// In this synthetic data:
+// 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
-+//
-+// The first Zone line has STDOFF=0 and uses rule set d. The May rule
-+// sets save=1 (WEST, total +1). zic.c interprets the wall UNTIL
-+// "1977 O 21" using the SAVE value in force just before the boundary
-+// (i.e. May's save=1, since the Oct-21 rule in this line's frame
fires
-+// at exactly the boundary, after the wall time has been read). So:
-+// wall(0:00) - stdoff(0) - save_just_before(1) = Oct 20 23:00 UTC
-+// is the correct UTC instant of the line's end.
-+//
-+// The second Zone line has STDOFF=1 and uses the same rule set d. In
-+// its own frame, the Oct-21 rule fires at exactly its starting
instant
-+// (Oct 20 23:00 UTC = wall(0:00) - stdoff(1)), setting save=0. So at
-+// Oct 20 23:00 UTC the new line begins with stdoff=1, save=0, abbrev
-+// "CET", total offset +1. Both sides of the boundary have total +1;
-+// only the (stdoff, save) split changes. This is the merge that
-+// zdump shows for real Africa/Algiers in October 1977.
+
+#include <chrono>
+#include <fstream>
@@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)
+ VERIFY( pre.save == 1h );
+ VERIFY( pre.abbrev == "WEST" );
+
-+ // The "active rule just before the wall UNTIL" is May-6 (save=1),
-+ // so the wall UNTIL "1977 O 21" gets adjusted by stdoff(0)+save(1).
-+ // Without the fix, master leaves the line's m_until 1 hour too late
-+ // and the query just before Oct 21 00:00 UTC is in the wrong frame.
-+ // With the fix, queries strictly before the boundary stay in the
-+ // first line (WEST) and queries at/after the boundary are in the
-+ // second line (CET).
++ // 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 );
@@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)
+ VERIFY( after.save == 0min );
+ VERIFY( after.abbrev == "CET" );
+
-+ // And a regression check that the boundary really moved: a query at
-+ // 1977-10-20 23:30 UTC must be in the SECOND line. Without the
-+ // fix, master's m_until for the first line is 1977-10-21 00:00 UTC,
-+ // and this query lands in the WET stretch produced by the first
-+ // line's expansion of the Oct-21 rule.
++ // 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" );
4: d94019806 ! 4: 9a01b2555 libstdc++: Cascade wall-time saves in lazy
expansion seeding [PR 124853]
@@
## Metadata ##
-Author: Alvaro Begue <alvaro.begue@gmail.com>
+Author: Álvaro Begué <alvaro.begue@gmail.com>
## Commit message ##
- libstdc++: Cascade wall-time saves in lazy expansion seeding [PR
124853]
+ libstdc++: Cascade wall-time saves in lazy expansion seeding
[PR124853]
When _M_get_sys_info seeds a Zone line by looking up the active
rule
just before info.begin, the previous code interpreted each rule in
@@ Commit message
libstdc++-v3/ChangeLog:
PR libstdc++/124853
- * src/c++20/tzdb.cc (time_zone::_Impl::_M_get_sys_info):
+ * src/c++20/tzdb.cc (time_zone::_M_get_sys_info):
Replace the per-rule isolated active-rule search with a
chronological cascade walker that maintains a running save
and interprets Wall-time rules' at_time relative to it.
@@ Commit message
"earliest STD rule" fallback to its own branch.
* testsuite/std/time/time_zone/wall_cascade.cc: New test.
+ Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
+
## libstdc++-v3/src/c++20/tzdb.cc ##
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
#endif
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- // 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
-- // expressed in the "save=0" frame (i.e. the parsed wall UNTIL
with
-- // the line's STDOFF subtracted) and `stdoff` is the line's
standard
-- // offset.
-+ // Find the Rule in `rules` whose effect was last in force at time
-+ // `t`, given that `stdoff` is the standard offset of the
enclosing
-+ // zone line. Returns nullptr if no rule fired strictly before t.
+- // with STDOFF subtracted and `stdoff` is the line's standard
offset.
++ // Find the Rule whose effect was last in force at time `t`, given
++ // that `stdoff` is the standard offset of the enclosing zone
line.
++ // Returns nullptr if no rule fired strictly before t.
//
- // The function walks all (rule, year) pairs in chronological
order,
- // maintaining a running save value. Wall-time rules have their
TIME
-@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- // zic.c's outzone() logic and is required for zone lines whose
- // rule set has rules whose at_time depends on cascading saves.
+ // 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.
//
-+ // Canonical case: Europe/Paris around 1945, where the France
rules
-+ // 1945 Apr 2 02:00 wall save=2 M
-+ // 1945 Sep 16 03:00 wall save=0 -
-+ // chain together: in the (stdoff=1, save=2) frame the September
-+ // rule fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.
-+ //
-+ // The calendar window is extended by one year on each side, to
-+ // catch rules whose wall-time at_time falls in early January or
-+ // late December but whose UT firing crosses a year boundary due
-+ // to a large stdoff or save (Pacific/Auckland's "1946 Ja 1" rule
-+ // in stdoff=12h fires at 1945-12-31 11:30 UT).
+ // 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>
+ const Rule*
+ find_active_rule(const _RuleRange& rules, sys_seconds t,
seconds stdoff)
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+
+ // 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
-+ // expressed in the "save=0" frame (i.e. the parsed wall UNTIL
with
-+ // the line's STDOFF subtracted) and `stdoff` is the line's
standard
-+ // offset.
++ // with STDOFF subtracted and `stdoff` is the line's standard
offset.
+ //
-+ // Like find_active_rule, walks (rule, year) pairs chronologically
-+ // with a running save value, but interprets `t` differently.
-+ //
- // The comparison `fire < boundary` shrinks `boundary` as the
running
- // save cascades up: a rule that fires AT the boundary (with the
- // cascaded save applied) is treated as belonging to the next zone
-@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- // of line 6 ("0 d WE%sT 1977 O 21"). The pre-rule save (1h, from
- // the May 6 rule) is what determines the boundary's UT placement,
- // not the Oct 21 rule's save=0.
-- //
-- // The calendar window is extended by one year on each side, to
-- // catch rules whose wall-time at_time falls in early January or
-- // late December but whose UT firing crosses a year boundary due
-- // to a large stdoff or save.
++ // Like find_active_rule but `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.
template<typename _RuleRange>
const Rule*
find_pre_until_rule(const _RuleRange& rules,
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's
- // frame, which is exactly the new line's begin.
+ // 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));
@@ libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc (new)
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
-+// Regression test: when lazy expansion seeds a Zone line whose rule
set
-+// has wall-time rules whose effective firing time depends on a prior
-+// rule's save (cascading wall-time interpretation), the seeding code
-+// must walk the rules chronologically with a running save value, not
-+// look at each rule in isolation against a fixed `save=0` frame.
-+//
-+// Mirrors the Europe/Paris 1945 case. France's rules
++// Wall-time rules in the same rule set whose effective firing time
++// depends on a prior rule's save (Europe/Paris 1945):
+// 1945 Apr 2 02:00 wall save=2 M
+// 1945 Sep 16 03:00 wall save=0 -
-+// chain together: in the (stdoff=1, save=2) frame the September rule
-+// fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.
-+//
-+// Construct a synthetic two-line zone whose second line begins
between
-+// those two interpretations of the September rule, so the seeding has
-+// to choose: with the cascade, the September rule has already fired
at
-+// info.begin and the new line seeds with save=0; without the cascade,
-+// the September rule appears not to have fired yet and the new line
-+// would (incorrectly) seed with save=2 from the April rule.
++// In the (stdoff=1, save=2) frame the September rule fires at
++// Sep 16 00:00 UT, not Sep 16 02:00 UT.
+
+#include <chrono>
+#include <fstream>
@@ libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc (new)
+ // Line 1 ends at "1945 Sep 16 1u" (Universal time, no save
shenanigans),
+ // so info.begin for line 2 is exactly 1945-09-16 01:00 UT.
+ //
-+ // Cascade seeding: in line 2's frame (stdoff=1):
-+ // * Apr 2 fires at Apr 2 01:00 UT (running save 0 → 2)
-+ // * Sep 16 fires at Sep 16 00:00 UT (running save 2 → 0)
-+ // Both fire before info.begin (Sep 16 01:00 UT), so the active rule
-+ // at info.begin is the Sep 16 one, save=0 → CET, total offset 1h.
-+ //
-+ // Without cascade: Sep 16 is computed as 03:00 - stdoff(1) = 02:00
UT,
-+ // which is *after* info.begin, so the seeding falls back to the
April
-+ // rule's save=2 → CEMT, total offset 3h. That is the bug.
++ // Two-line zone whose second line begins at 1945 Sep 16 01:00 UT,
++ // between the cascaded firing time (Sep 16 00:00 UT) and the
++ // non-cascaded firing time (Sep 16 02:00 UT) of the September rule.
++ // The seeding must pick the September rule (save=0, CET) at
info.begin.
+ std::ofstream("tzdata.zi") << R"(# version test_wall_cascade
+R Fr 1945 o - Apr 2 2 2 M
+R Fr 1945 o - Sep 16 3 0 -
5: ab7938201 ! 5: 1c717d45a libstdc++: Implement zic writezone merge
optimization [PR 124854]
@@
## Metadata ##
-Author: Alvaro Begue <alvaro.begue@gmail.com>
+Author: Álvaro Begué <alvaro.begue@gmail.com>
## Commit message ##
- libstdc++: Implement zic writezone merge optimization [PR 124854]
+ libstdc++: Implement zic writezone merge optimization [PR124854]
Two distinct correctness fixes that together let lazy expansion
match
zic.c's writezone output for zones with rule firings near zone-line
@@ Commit message
libstdc++-v3/ChangeLog:
PR libstdc++/124854
- * src/c++20/tzdb.cc (time_zone::_Impl::_M_get_sys_info):
+ * src/c++20/tzdb.cc (time_zone::_M_get_sys_info):
Always run find_active_rule to seed info.offset and
info.save,
regardless of whether letters was already populated from
i[-1].next_letters(). Add a writezone merge optimization
@@ Commit message
re-entry.
* testsuite/std/time/time_zone/zone_merge.cc: New test.
+ Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
+
## libstdc++-v3/src/c++20/tzdb.cc ##
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
if (i != infos.begin() && i[-1].expanded())
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- if (letters.empty())
- {
-- // We want the rule whose effect is in force at info.begin --
-- // including a rule that fires at exactly info.begin (its effect
-- // has just begun and is active for the first sys_info we are
-- // about to generate). The search below uses a strict
-- // `rule_start < t` comparison, so pass info.begin + 1s to make
-- // the half-open lookup (..., info.begin] inclusive of the
-- // boundary instant. This is what makes named-rule zone lines
-- // like Africa/Algiers (PR 116110) seed with the correct save:
-- // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's
-- // frame, which is exactly the new line's begin.
+- // 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);
-
- // Try to find a Rule active before this time, to get initial
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
- {
- info.offset = ri.offset() + active_rule->save;
- info.save = chrono::duration_cast<minutes>(active_rule->save);
-+ // Seed info.offset and info.save from the rule whose effect is in
-+ // force at info.begin. This must run even when `letters` was
-+ // already populated from i[-1].next_letters() (which happens
-+ // during a re-entry of partial lazy expansion: the previous batch
-+ // left an expanded ZoneInfo whose next_letters() field is the
-+ // letters for the first sys_info of this batch), because
-+ // info.offset/save are still at their stdoff/0 init values and
-+ // would otherwise carry through into the first emitted sys_info
-+ // with the wrong total offset.
++ // Seed info.offset and info.save from the rule active at
++ // info.begin. Always run this (even when `letters` was populated
++ // from i[-1].next_letters() during partial-expansion re-entry),
++ // because info.offset/save are still at their stdoff/0 init
values
++ // and would otherwise produce the wrong total offset.
+ //
-+ // The search uses a strict `rule_start < t` comparison, so pass
-+ // info.begin + 1s to make the half-open lookup (..., info.begin]
-+ // inclusive of the boundary instant. This is what makes named-
-+ // rule zone lines like Africa/Algiers (PR 116110) seed with the
-+ // correct save: the Oct-21 rule fires at Oct 20 23:00 UTC in the
-+ // new line's frame, which is exactly the new line's begin.
++ // 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 Rule* active_rule = find_active_rule(rules, t,
ri.offset());
@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono
+ }
+ }
+
-+ // zic.c writezone merge optimization. When the previous zone
-+ // line's end total offset differs from this line's seeded total
-+ // and the local time would jump backward at the boundary, look
-+ // for a rule in this line's set that fires within the resulting
-+ // gap window and would compensate the jump. zic folds such a
-+ // rule into the boundary transition; we mirror that here by
-+ // pulling the rule's save back to info.begin.
-+ //
-+ // Only runs at the first sys_info of a zone line (not on re-entry
-+ // mid-line during partial lazy expansion), which is detected by
-+ // an empty next_letters() on the prior expanded ZoneInfo: a mid-
-+ // line re-entry's prior ZoneInfo always has a non-empty
-+ // next_letters() (the letters of the rule that fires at the new
-+ // batch's start), whereas a zone-line transition's prior ZoneInfo
-+ // ends with empty next_letters() because the line's last
-+ // iteration emits with letters cleared.
++ // zic.c writezone merge optimization. When the local time jumps
++ // backward at a zone-line boundary and a rule in the new line's
set
++ // fires within that gap window, fold the rule's save into the
++ // boundary so the new line begins with the post-rule save.
+ //
-+ // Canonical examples:
-+ // * America/Argentina/Buenos_Aires 1999-10-03: lines
-+ // -3 A -03/-02 1999 O 3
-+ // -4 A -04/-03 2000 Mar 3
-+ // have new_total = -4, prev_total = -3 (jump = -1h). The
-+ // 1999 Oct Argentina rule fires at Oct 3 04:00 UTC in the
-+ // new -4 frame, which is exactly info.begin + 1h, inside
-+ // the 1h window.
-+ // * Europe/Berlin 1945-05-24: lines
-+ // 1 c CE%sT 1945 May 24 2
-+ // 1 So CE%sT 1946
-+ // have new_total = 1, prev_total = 2 (jump = -1h). The So
-+ // 1945-May-24 rule (save = 2h, "CEMT") fires at 01:00 UTC
-+ // in the new frame, inside the 1h window.
++ // Only runs at the first sys_info of a zone line (not on partial-
++ // expansion re-entry mid-line): a mid-line re-entry's prior
++ // ZoneInfo has a non-empty next_letters(), while a zone-line
++ // transition's prior ZoneInfo ends with empty next_letters().
+ if (i != infos.begin() && i[-1].expanded()
+ && i[-1].next_letters().empty())
+ {
@@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
-+// Regression test: zic.c's writezone merges a zone-line transition
with
-+// a rule firing that would otherwise create a brief observably-wrong
-+// stretch of local time. 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 = new_total - old_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.
-+//
-+// Two canonical real-world cases:
-+// * America/Argentina/Buenos_Aires 1999-10-03 (lines change stdoff
-+// -3 → -4 with an Argentina DST rule firing on the same day).
-+// * Europe/Berlin 1945-05-24 (lines split a rule set, with the So
-+// 1945-May-24 rule firing inside the boundary's window).
++// 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.
+//
-+// Mirror the Buenos Aires shape with a synthetic zone.
++// Mirrors America/Argentina/Buenos_Aires around 1999-10-03.
+
+#include <chrono>
+#include <fstream>
@@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)
+{
+ using namespace std::chrono;
+
-+ // Argentina-style: stdoff jumps from -3 to -4 at the same instant
-+ // a save=1 ("S") rule fires. In the new (-4) frame, the rule's
-+ // wall at_time of 00:00 is at UT 04:00, which is 1 hour after the
-+ // boundary at UT 03:00. Without the merge optimization the new
-+ // line would seed with save=0 (offset -4, abbrev -04) for that 1
-+ // hour and then transition to save=1 (offset -3, abbrev -03);
-+ // with the merge, the boundary itself is at offset=-3, save=1.
++ // 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 -
@@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)
+ VERIFY( before.save == 0min );
+ VERIFY( before.abbrev == "-03" );
+
-+ // At the boundary the merge optimization kicks in: the second zone
-+ // line's first sys_info should already have save=1 from the Oct 3
-+ // rule, total offset -3h, abbrev "-03". Without the fix, chrono
-+ // would emit a 1-hour stretch of save=0 ("-04") here.
++ // 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 );
--
2.34.1
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260426/14226491/attachment-0001.htm>
More information about the Libstdc++
mailing list