[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