On Tue, May 12, 2026 at 3:23 PM Jonathan Wakely <[email protected]> wrote:
> On Tue, 12 May 2026 at 14:49 +0200, Tomasz Kamiński wrote: > >From: Álvaro Begué <[email protected]> > > > >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 <[email protected]> > >Co-authored-by: Tomasz Kamiński <[email protected]> > >Signed-off-by: Álvaro Begué <[email protected]> > >Signed-off-by: Tomasz Kamiński <[email protected]> > >--- > >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 > > > > > >
