[PATCH v3] libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]
Tomasz Kaminski
tkaminsk@redhat.com
Wed Jul 22 15:08:49 GMT 2026
On Tue, May 12, 2026 at 3:23 PM Jonathan Wakely <jwakely@redhat.com> wrote:
> On Tue, 12 May 2026 at 14:49 +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_day&) as operator>> for newly added
> >on_day_t tag type, and reuse it for the UNTIL DAY field. The operator
> >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:
> >- removed if (!ws(in).eof())
> >- add test_year_month_day_only test, without time
> >- expanded all *_only test to cover with comment
>
> Great, thanks for the changes. OK for trunk.
>
> (I think we should backport this to 16.2 or 16.3 eventually as well)
>
I am going to backport this for GCC 16.2
>
> >
> >Testing on x86_64-linux. All *time_zone* test passed.
> >
> > libstdc++-v3/src/c++20/tzdb.cc | 76 ++++--
> > .../std/time/time_zone/until_day_on.cc | 222 ++++++++++++++++++
> > 2 files changed, 272 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..8d108523a4a 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,25 @@ 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.
> >+ 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);
> >+ using enum on_month_day::Kind;
> >+
> >+ 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 = DayOfMonth;
> > on.day_of_month = d;
> >- to = on;
> > return in;
> > }
> > }
> >@@ -2224,9 +2235,8 @@ namespace std::chrono
> > in.ignore(4);
> > if (abbrev_weekday w{}; in >> w) [[likely]]
> > {
> >- on.kind = on_day::LastWeekday;
> >+ on.kind = LastWeekday;
> > on.day_of_week = w.wd.c_encoding();
> >- to = on;
> > return in;
> > }
> > }
> >@@ -2238,14 +2248,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 == '<' ? LessEq : GreaterEq;
> >+ on.day_of_week = w.wd.c_encoding();
> > on.day_of_month = d;
> >- to = on;
> > return in;
> > }
> > }
> >@@ -2255,6 +2264,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 +2377,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);
> >+ 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..38e4bc254cc
> >--- /dev/null
> >+++ b/libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
> >@@ -0,0 +1,222 @@
> >+// { 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
> >+Z Test/YearOnlyC 4 - C 1995 # comment
> >+ 4 - D
> >+)";
> >+
> >+ 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");
> >+ sys_seconds boundary = sys_days{1990y/January/1};
> >+ auto before = tz->get_info(boundary - 1s);
> >+ VERIFY( before.abbrev == "A" );
> >+ auto at = tz->get_info(boundary);
> >+ VERIFY( at.abbrev == "B" );
> >+
> >+ tz = locate_zone("Test/YearOnlyC");
> >+ boundary = sys_days{1995y/January/1} - 4h;
> >+ before = tz->get_info(boundary - 1s);
> >+ VERIFY( before.abbrev == "C" );
> >+ at = tz->get_info(boundary);
> >+ VERIFY( at.abbrev == "D" );
> >+}
> >+
> >+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
> >+Z Test/YearMonthC 3 - C 1995 Apr # comment
> >+ 3 - D
> >+)";
> >+
> >+ 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");
> >+ sys_seconds boundary = sys_days{1990y/July/1};
> >+ auto before = tz->get_info(boundary - 1s);
> >+ VERIFY( before.abbrev == "A" );
> >+ auto at = tz->get_info(boundary);
> >+ VERIFY( at.abbrev == "B" );
> >+
> >+ tz = locate_zone("Test/YearMonthC");
> >+ boundary = sys_days{1995y/April/1} - 3h;
> >+ before = tz->get_info(boundary - 1s);
> >+ VERIFY( before.abbrev == "C" );
> >+ at = tz->get_info(boundary);
> >+ VERIFY( at.abbrev == "D" );
> >+}
> >+
> >+void
> >+test_year_month_day_only()
> >+{
> >+ using namespace std::chrono;
> >+
> >+ std::ofstream("tzdata.zi") << R"(# version test_day_only
> >+Z Test/DayOnly 0 - A 1997 Mar 12
> >+ 0 - B
> >+Z Test/DayOnlyC 5 - C 1998 Jun 14 # comment
> >+ 5 - D
> >+)";
> >+
> >+ const auto& db = reload_tzdb();
> >+ VERIFY( override_used ); // If this fails then XFAIL for the target.
> >+ VERIFY( db.version == "test_day_only" );
> >+
> >+ auto* tz = locate_zone("Test/DayOnly");
> >+ sys_seconds boundary = sys_days{1997y/March/12};
> >+ auto before = tz->get_info(boundary - 1s);
> >+ VERIFY( before.abbrev == "A" );
> >+ auto at = tz->get_info(boundary);
> >+ VERIFY( at.abbrev == "B" );
> >+
> >+ tz = locate_zone("Test/DayOnlyC");
> >+ boundary = sys_days{1998y/June/14} - 5h;
> >+ before = tz->get_info(boundary - 1s);
> >+ VERIFY( before.abbrev == "C" );
> >+ at = tz->get_info(boundary);
> >+ VERIFY( at.abbrev == "D" );
> >+}
> >+
> >+int
> >+main()
> >+{
> >+ test_lastsu();
> >+ test_sun_ge_n();
> >+ test_sun_le_n();
> >+ test_year_only();
> >+ test_year_month_only();
> >+ test_year_month_day_only();
> >+}
> >--
> >2.54.0
> >
> >
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260722/bdb37434/attachment-0001.htm>
More information about the Libstdc++
mailing list