[PATCH v2] libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]
Jonathan Wakely
jwakely@redhat.com
Tue May 12 10:40:56 GMT 2026
On Tue, 12 May 2026 at 11:35 +0200, Tomasz Kamiński wrote:
>From: Álvaro Begué <alvaro.begue@gmail.com>
>
>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
>forms as Rule lines: lastSun, Sun>=8, Sat<=20, etc. Real zones use these
>forms in their UNTIL DAY: Europe/Simferopol's `3 - MSK 1997 Mar lastSu
>1u`, for instance, became `Mar 1` (silently misparsed) instead of `Mar
>30`, leaving Simferopol an extra 29 days in MSK.
>
>The previous parser's `int d = 1; in >> m >> d >> t;` chain silently
>left d == 1 when the day token wasn't a digit, then went on to parse the
>remainder as the TIME field.
>
>Renames on_day to on_month_day, and factor out the day-component parser
>parser from operator>>(istream&, on_month_day&) as operator>> for newly
>added on_day_t tag type, and reuse it for the UNTIL DAY field. It
>handles all three on_day forms (DayOfMonth, LastWeekday, LessEq /
>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
>attempt to parse a DAY if any non-whitespace remains after the MONTH.
>
>libstdc++-v3/ChangeLog:
>
> PR libstdc++/124852
> * src/c++20/tzdb.cc (on_day): Rename to...
> (on_day_month): Rename from on_day.
> (on_day_month::on_day_t, on_day_month::on_day): Define.
> (operator>>(istream&, on_day_t&&)): Factored out of
> operator>>(istream&, on_day&).
> (operator>>(istream&, on_day&)): Use on_day_t parser.
> (operator>>(istream&, ZoneInfo&)): Replace the integer DAY
> parser with on_day_t for the UNTIL field.
> * testsuite/std/time/time_zone/until_day_on.cc: New test.
>
>Reviewed-by: Jonathan Wakely <jwakely@redhat.com>
>Co-authored-by: Tomasz Kamiński <tkaminsk@redhat.com>
>Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>Signed-off-by: Tomasz Kamiński <tkaminsk@redhat.com>
>---
>v2 changes:
> - rename on_day to on_month_day for clarity
> - introduce on_day_t/on_day helpers and replace parse_day_spec
> with operator>> for on_day_t
> - adds comment on VERIFY( override_used ) checks
>I think on_month_day is better name (it is always day of month),
>also store reference in on_day_t helper, so I do not need to wonder
>about nulls.
>
>Testing on x86_64-linux. The *tzdb* passed.
>OK for trunk when all test passes?
>
> libstdc++-v3/src/c++20/tzdb.cc | 74 +++++---
> .../std/time/time_zone/until_day_on.cc | 170 ++++++++++++++++++
> 2 files changed, 218 insertions(+), 26 deletions(-)
> create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
>
>diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
>index e9ce120622c..96c6b462d18 100644
>--- a/libstdc++-v3/src/c++20/tzdb.cc
>+++ b/libstdc++-v3/src/c++20/tzdb.cc
>@@ -214,7 +214,7 @@ namespace std::chrono
> constinit atomic<tzdb_list::_Node*> tzdb_list::_Node::_S_head_cache{nullptr};
> #endif
>
>- // The data structures defined in this file (Rule, on_day, at_time etc.)
>+ // The data structures defined in this file (Rule, on_month_day, at_time etc.)
> // are used to represent the information parsed from the tzdata.zi file
> // described at https://man7.org/linux/man-pages/man8/zic.8.html#FILES
>
>@@ -303,7 +303,7 @@ namespace std::chrono
> };
>
> // The IN and ON fields of a RULE record, e.g. "March lastSunday".
>- struct on_day
>+ struct on_month_day
> {
> using rep = uint_least16_t;
> // Equivalent to Kind, chrono::month, chrono::day, chrono::weekday,
>@@ -367,7 +367,15 @@ namespace std::chrono
> return ymd;
> }
>
>- friend istream& operator>>(istream&, on_day&);
>+ struct on_day_t // tag type for reading ON and DAY fields only
>+ {
>+ on_month_day& parent;
>+ friend istream& operator>>(istream&, on_day_t&&);
>+ };
>+
>+ on_day_t on_day() { return on_day_t{*this}; }
>+
>+ friend istream& operator>>(istream&, on_month_day&);
> };
>
> // Wrapper for two chrono::year values, which reads the FROM and TO
>@@ -590,9 +598,9 @@ namespace std::chrono
> // A RULE record from the tzdata.zi timezone info file.
> struct Rule
> {
>- // This allows on_day to reuse padding of at_time.
>+ // This allows on_month_day to reuse padding of at_time.
> // This keeps the size to 8 bytes and the alignment to 4 bytes.
>- struct datetime : at_time { on_day day; };
>+ struct datetime : at_time { on_month_day day; };
>
> // TODO combining name+letters into a single string (like in ZoneInfo)
> // would save sizeof(string) and make Rule fit in a single cacheline.
>@@ -654,17 +662,17 @@ namespace std::chrono
> << ' ' << r.when.day.get_month() << ' ';
> switch (r.when.day.kind)
> {
>- case on_day::DayOfMonth:
>+ case on_month_day::DayOfMonth:
> out << (unsigned)r.when.day.get_day();
> break;
>- case on_day::LastWeekday:
>+ case on_month_day::LastWeekday:
> out << "last" << weekday(r.when.day.day_of_week);
> break;
>- case on_day::LessEq:
>+ case on_month_day::LessEq:
> out << weekday(r.when.day.day_of_week) << " <= "
> << r.when.day.day_of_month;
> break;
>- case on_day::GreaterEq:
>+ case on_month_day::GreaterEq:
> out << weekday(r.when.day.day_of_week) << " >= "
> << r.when.day.day_of_month;
> break;
>@@ -2200,22 +2208,23 @@ namespace std::chrono
> }
> };
>
>- istream& operator>>(istream& in, on_day& to)
>+ // Read the day-component of an on_month_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). On failure the function sets failbit
>+ // and leaves `to.parent` unchanged.
This last sentence is no longer true, it does not leave to.parent
unchanged, because it modifies it directly through a reference now.
I think it's OK to modify it, because the caller will not use the
struct if a parse error sets failbit. So we can just drop the part
that says "and leaves `to.parent` unchanged".
>+ istream& operator>>(istream& in, on_month_day::on_day_t&& to)
> {
>- on_day on{};
>- abbrev_month m{};
>- in >> m;
>- on.month = static_cast<unsigned>(m.m);
>+ on_month_day& on = to.parent;
> int c = ws(in).peek();
> if ('0' <= c && c <= '9')
> {
>- on.kind = on_day::DayOfMonth;
> unsigned d;
> in >> d;
> if (d <= 31) [[likely]]
> {
>+ on.kind = on_month_day::DayOfMonth;
> on.day_of_month = d;
>- to = on;
> return in;
> }
> }
>@@ -2224,9 +2233,8 @@ namespace std::chrono
> in.ignore(4);
> if (abbrev_weekday w{}; in >> w) [[likely]]
> {
>- on.kind = on_day::LastWeekday;
>+ on.kind = on_month_day::LastWeekday;
> on.day_of_week = w.wd.c_encoding();
>- to = on;
> return in;
> }
> }
>@@ -2238,14 +2246,13 @@ namespace std::chrono
> {
> if (in.get() == '=')
> {
>- on.kind = c == '<' ? on_day::LessEq : on_day::GreaterEq;
>- on.day_of_week = w.wd.c_encoding();
> unsigned d;
> in >> d;
> if (d <= 31) [[likely]]
> {
>+ on.kind = c == '<' ? on_month_day::LessEq : on_month_day::GreaterEq;
Is this line more than 80 chars now?
We could do `using enum on_month_day::Kind;` at the start of the
function.
>+ on.day_of_week = w.wd.c_encoding();
> on.day_of_month = d;
>- to = on;
> return in;
> }
> }
>@@ -2255,6 +2262,17 @@ namespace std::chrono
> return in;
> }
>
>+ istream& operator>>(istream& in, on_month_day& to)
>+ {
>+ on_month_day md{};
>+ abbrev_month m{};
>+ in >> m;
>+ md.month = static_cast<unsigned>(m.m);
>+ if (in >> md.on_day())
>+ to = md;
>+ return in;
>+ }
>+
> istream& operator>>(istream& in, at_time& at)
> {
> int sign = 1;
>@@ -2357,12 +2375,16 @@ 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};
>- int d = 1;
>+ on_month_day on{.kind = on_month_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 (abbrev_month m{January}; in >> m)
>+ {
>+ on.month = static_cast<unsigned>(m.m);
>+ if (!ws(in).eof())
The condition above is only true for lines with trailing whitespace,
but we should also handle trailing comments:
# Zone NAME STDOFF RULES FORMAT [UNTIL]
Zone Test/Comment -5:00 - EST 1973 Apr # default DAY+TIME
A comment after the MONTH should be ignored, and be treated the same
as a line with no DAY and TIME parts for UNTIL (i.e. DAY=1 and
TIME=00:00). But the code above will attempt to read '#' as a DAY and
then set failbit.
Could you add a test for this case please?
I think the code works anyway, because if we set failbit when reading
the DAY then that's harmless because we don't actually check in.good()
again after that. But maybe it should be:
if (ws(in).peek() != '#' && !in.eof())
in >> on.on_day() >> t;
Alternatively we could make the operator>> for on_day_t gracefully
handle the case where the rest of the line is just whitespace or
comments, so we wouldn't need the 'if' at all. If there are no input
fields for DAY then it should just set eofbit to indicate no more
input. That would probably make the code that uses operator>> simpler
and easier to read.
I think the operator>> overload for at_time could also be fixed to
handle this similar case:
# Zone NAME STDOFF RULES FORMAT [UNTIL]
Zone Test/Comment -5:00 - EST 1973 Apr 1 # default TIME
It looks like it will set failbit when it hits a comment character,
but again the code still works because we never check in.good() after
attempting to read the DAY and TIME fields.
>+ in >> on.on_day() >> t;
>+ }
>+ year_month_day ymd = on.pin(year(y));
>+ inf.m_until = sys_days(ymd) + seconds(t.time);
> if (t.indicator != at_time::Universal)
> { // UNTIL uses "the rules in effect just before the transition"
> // adjust by STDOFF
>diff --git a/libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc b/libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
>new file mode 100644
>index 00000000000..70879b40e34
>--- /dev/null
>+++ b/libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
>@@ -0,0 +1,170 @@
>+// { 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* } }
>+
>+// 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 places the boundary on 1997-03-30 (the last Sunday of March).
>+
>+#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 "./";
>+ }
>+}
>+
>+void
>+test_lastsu()
>+{
>+ using namespace std::chrono;
>+
>+ std::ofstream("tzdata.zi") << R"(# version test_lastsu
>+Z Test/LastSu 3 - MSK 1997 Mar lastSu 1u
>+ 3 - X
>+)";
>+
>+ const auto& db = reload_tzdb();
>+ VERIFY( override_used ); // If this fails then XFAIL for the target.
>+ VERIFY( db.version == "test_lastsu" );
>+
>+ auto* tz = locate_zone("Test/LastSu");
>+
>+ // True boundary: 1997-03-30 01:00 UTC (lastSu of March 1997 is Mar 30,
>+ // and the indicator is 'u' = Universal so no offset adjustment).
>+ sys_seconds boundary = sys_days{1997y/March/30} + 1h;
>+
>+ // Just before: still in the MSK line.
>+ auto before = tz->get_info(boundary - 1s);
>+ VERIFY( before.abbrev == "MSK" );
>+ VERIFY( before.offset == 3h );
>+
>+ // At/after the boundary: in the X line.
>+ auto at = tz->get_info(boundary);
>+ VERIFY( at.abbrev == "X" );
>+
>+ // 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 );
>+}
>+
>+void
>+test_sun_ge_n()
>+{
>+ using namespace std::chrono;
>+
>+ std::ofstream("tzdata.zi") << R"(# version test_sun_ge_n
>+Z Test/SunGE 0 - A 1990 Jun Sun>=8 0u
>+ 0 - B
>+)";
>+
>+ const auto& db = reload_tzdb();
>+ VERIFY( override_used ); // If this fails then XFAIL for the target.
>+ VERIFY( db.version == "test_sun_ge_n" );
>+
>+ auto* tz = locate_zone("Test/SunGE");
>+
>+ // First Sunday >= June 8 1990 = June 10 (June 8 1990 was a Friday).
>+ sys_seconds boundary = sys_days{1990y/June/10};
>+
>+ auto before = tz->get_info(boundary - 1s);
>+ VERIFY( before.abbrev == "A" );
>+ auto at = tz->get_info(boundary);
>+ VERIFY( at.abbrev == "B" );
>+
>+ // 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" );
>+}
>+
>+void
>+test_sun_le_n()
>+{
>+ using namespace std::chrono;
>+
>+ std::ofstream("tzdata.zi") << R"(# version test_sun_le_n
>+Z Test/SunLE 0 - A 1990 Jun Sun<=15 0u
>+ 0 - B
>+)";
>+
>+ const auto& db = reload_tzdb();
>+ VERIFY( override_used ); // If this fails then XFAIL for the target.
>+ VERIFY( db.version == "test_sun_le_n" );
>+
>+ auto* tz = locate_zone("Test/SunLE");
>+
>+ // Last Sunday <= June 15 1990 = June 10.
>+ sys_seconds boundary = sys_days{1990y/June/10};
>+
>+ auto before = tz->get_info(boundary - 1s);
>+ VERIFY( before.abbrev == "A" );
>+ auto at = tz->get_info(boundary);
>+ VERIFY( at.abbrev == "B" );
>+}
>+
>+void
>+test_year_only()
>+{
>+ using namespace std::chrono;
>+
>+ // 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
>+)";
>+
>+ const auto& db = reload_tzdb();
>+ VERIFY( override_used ); // If this fails then XFAIL for the target.
>+ VERIFY( db.version == "test_year_only" );
>+
>+ auto* tz = locate_zone("Test/YearOnly");
>+ auto before = tz->get_info(sys_days{1989y/December/31} + 23h);
>+ VERIFY( before.abbrev == "A" );
>+ auto at = tz->get_info(sys_days{1990y/January/1});
>+ VERIFY( at.abbrev == "B" );
>+}
>+
>+void
>+test_year_month_only()
>+{
>+ using namespace std::chrono;
>+
>+ // 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
>+)";
>+
>+ const auto& db = reload_tzdb();
>+ VERIFY( override_used ); // If this fails then XFAIL for the target.
>+ VERIFY( db.version == "test_year_month_only" );
>+
>+ auto* tz = locate_zone("Test/YearMonth");
>+ auto before = tz->get_info(sys_days{1990y/June/30} + 23h);
>+ VERIFY( before.abbrev == "A" );
>+ auto at = tz->get_info(sys_days{1990y/July/1});
>+ VERIFY( at.abbrev == "B" );
>+}
>+
>+int
>+main()
>+{
>+ test_lastsu();
>+ test_sun_ge_n();
>+ test_sun_le_n();
>+ test_year_only();
>+ test_year_month_only();
>+}
>--
>2.54.0
>
>
More information about the Libstdc++
mailing list