https://gcc.gnu.org/g:2b5e55eea69acbc6e46fa3463164e62bcc436e0d
commit r17-486-g2b5e55eea69acbc6e46fa3463164e62bcc436e0d Author: Álvaro Begué <[email protected]> Date: Tue May 12 14:55:01 2026 +0200 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 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]> Diff: --- libstdc++-v3/src/c++20/tzdb.cc | 76 ++++--- .../testsuite/std/time/time_zone/until_day_on.cc | 222 +++++++++++++++++++++ 2 files changed, 272 insertions(+), 26 deletions(-) diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc index 886d7e71d8eb..f9238542274b 100644 --- a/libstdc++-v3/src/c++20/tzdb.cc +++ b/libstdc++-v3/src/c++20/tzdb.cc @@ -218,7 +218,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 @@ -307,7 +307,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, @@ -371,7 +371,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 @@ -594,9 +602,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. @@ -658,17 +666,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; @@ -2326,22 +2334,25 @@ namespace } }; - 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; } } @@ -2350,9 +2361,8 @@ namespace 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; } } @@ -2364,14 +2374,13 @@ namespace { 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; } } @@ -2381,6 +2390,17 @@ namespace 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; @@ -2483,12 +2503,16 @@ namespace 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 000000000000..38e4bc254cc1 --- /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(); +}
