[PATCH 2/5] libstdc++: Support ON-format DAY in Zone UNTIL field [PR 124852]
Tomasz Kaminski
tkaminsk@redhat.com
Wed Apr 15 12:52:42 GMT 2026
On Sat, Apr 11, 2026 at 3:35 PM Alvaro Begue <alvaro.begue@gmail.com> wrote:
> 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.
>
> Fix by reusing the existing parse_on_day_body() helper that already
> 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.
>
> 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 (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.
> (operator>>(istream&, ZoneInfo&)): Replace the integer DAY
> parser with parse_on_day_body for the UNTIL field.
> * testsuite/std/time/time_zone/until_day_on.cc: New test.
> ---
>
Again, only stylistic and comments changes. The implementation
looks really good,
thank you for working on it.
> libstdc++-v3/src/c++20/tzdb.cc | 55 ++++--
> .../std/time/time_zone/until_day_on.cc | 177 ++++++++++++++++++
> 2 files changed, 217 insertions(+), 15 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 33217f0a9..8cbf3213d 100644
> --- a/libstdc++-v3/src/c++20/tzdb.cc
> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> @@ -2204,22 +2204,24 @@ namespace std::chrono
> }
> };
>
> - istream& operator>>(istream& in, on_day& to)
> + // 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.
>
The line "The caller is responsible for setting `on.month` before calling.
", this function
does not use on.month value, so I do not think it need to be set before
calling this function.
> + istream&
> + parse_on_day_body(istream& in, on_day& on)
>
Would name it to parse_day_spec.
> {
> - on_day on{};
> - abbrev_month m{};
> - in >> m;
> - on.month = static_cast<unsigned>(m.m);
> 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_day::DayOfMonth;
> on.day_of_month = d;
> - to = on;
> return in;
> }
> }
> @@ -2230,7 +2232,6 @@ namespace std::chrono
> {
> on.kind = on_day::LastWeekday;
> on.day_of_week = w.wd.c_encoding();
> - to = on;
> return in;
> }
> }
> @@ -2242,14 +2243,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_day::LessEq :
> on_day::GreaterEq;
> + on.day_of_week = w.wd.c_encoding();
> on.day_of_month = d;
> - to = on;
> return in;
> }
> }
> @@ -2259,6 +2259,17 @@ namespace std::chrono
> return in;
> }
>
> + istream& operator>>(istream& in, on_day& to)
> + {
> + on_day on{};
> + abbrev_month m{};
> + in >> m;
> + on.month = static_cast<unsigned>(m.m);
> + if (parse_on_day_body(in, on))
> + to = on;
> + return in;
> + }
> +
> istream& operator>>(istream& in, at_time& at)
> {
> int sign = 1;
> @@ -2362,11 +2373,25 @@ namespace std::chrono
> if (int y = int(year::max()); in >> y)
> {
> abbrev_month m{January};
> - int d = 1;
> + on_day on{};
>
C++20 allows designated initializers, so I would use them here:
on-day on{.kind = on_day::DayOfMonth, .month =1, .day_of_mont = 1)
> + on.kind = on_day::DayOfMonth;
> + on.month = 1; // default January
> + on.day_of_month = 1; // default 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)
>
Use if with initializer to declare abbrev_month here:
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.
>
The grammar is quoted a few lines above, and I don't think this comment
adds much
value, so I would remove it.
> + if (!ws(in).eof())
> + parse_on_day_body(in, on);
>
The time can only follow the day, so I would do something like, to better
reflect that.
if (!ws(in).eof())
if (parse_on_day_body(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)
> { // 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 000000000..cf9e51d26
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
> @@ -0,0 +1,177 @@
> +// { 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* } }
> +
> +// 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.
Again I would remove the "Regressions test:" part, just keep the
description.
and remove everything before the real-world example.
> 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.
> +//
> +// 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.
> +
> +#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 );
> + 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" );
> +
> + // 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.
>
Again, I do not think the comments describing bugs are valuable, I would
add something like:
// Check if ON day is parsed correctly, and not default to 1st.
> + 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 );
> + 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" );
> +
> + // A June-1 query must still be in the A line (the unfixed parser
> + // placed the boundary on June 1).
>
Similary here
> + 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 );
> + 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;
> +
> + // Sanity check: a UNTIL with only a year (no MONTH, no DAY, no TIME)
> + // must continue to default to January 1 00:00.
>
Replace with:
// MONTH, DAY and TIME defaults 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( 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;
> +
> + // 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 defaults to 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( 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.34.1
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://gcc.gnu.org/pipermail/libstdc++/attachments/20260415/f0b46360/attachment-0001.htm>
More information about the Libstdc++
mailing list