[PATCH v2 2/5] libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]

Jonathan Wakely jwakely@redhat.com
Tue May 12 09:01:07 GMT 2026


On Sun, 26 Apr 2026 at 19:53 -0400, Álvaro Begué 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.
>
>Factor out the day-component parser from operator>>(istream&, on_day&)
>as parse_day_spec(), and reuse it for the UNTIL DAY field.  parse_day_spec
>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 (parse_day_spec): New function, factored
>out of operator>>(istream&, on_day&).
>(operator>>(istream&, on_day&)): Use parse_day_spec.
>(operator>>(istream&, ZoneInfo&)): Replace the integer DAY
>parser with parse_day_spec for the UNTIL field.
>* testsuite/std/time/time_zone/until_day_on.cc: New test.
>
>Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>---
> libstdc++-v3/src/c++20/tzdb.cc                |  47 +++--
> .../std/time/time_zone/until_day_on.cc        | 168 ++++++++++++++++++
> 2 files changed, 199 insertions(+), 16 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 1e49bb749..c0d62bc35 100644
>--- a/libstdc++-v3/src/c++20/tzdb.cc
>+++ b/libstdc++-v3/src/c++20/tzdb.cc
>@@ -2200,22 +2200,23 @@ 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).  On failure the function sets failbit
>+    // and leaves `on` unchanged.
>+    istream&
>+    parse_day_spec(istream& in, on_day& on)

This makes me regret naming the struct 'on_day' when it's not just the
ON field, it's the IN and ON fields combined.  If I'd named that with
a better name, we could have both:

   operator>>(istream&, in_on_day&);

and

   operator>>(istream&, on_day&);

instead of using operator>> for most types, and then having a named
'parse_day_spec' function for just one thing.

We can do this:

diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
index 587e88bc0dd5..5ad865a3e760 100644
--- a/libstdc++-v3/src/c++20/tzdb.cc
+++ b/libstdc++-v3/src/c++20/tzdb.cc
@@ -372,6 +372,14 @@ namespace std::chrono
        }
  
        friend istream& operator>>(istream&, on_day&);
+
+      struct day_t // tag type for reading ON and DAY fields only
+      {
+       on_day* parent;
+       friend istream& operator>>(istream&, day_t&&);
+      };
+
+      day_t day() { return day_t{this}; }
      };
  
      // Wrapper for two chrono::year values, which reads the FROM and TO

And then the parse_day_spec function becomes the operator>> for
on_day::day_t, and you would use it like this:

+    istream& operator>>(istream& in, on_day& to)
+    {
+      on_day on{};
+      abbrev_month m{};
+      in >> m;
+      on.month = static_cast<unsigned>(m.m);
+      if (in >> on.day())
+	 to = on;
+      return in;
+    }

I would prefer to consistently use operator>> for parsing, instead of
a mixture of operator>> and named functions.

>     {
>-      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;

All the leading TABs have been stripped from this patch again :-(

This makes it hard to review and even harder to apply the patch.

If you can't use git-send-email to send the patch, please attach a
patch.txt file in Gmail, which prevents it from being munged by Gmail.


>   unsigned d;
>   in >> d;
>   if (d <= 31) [[likely]]
>     {
>+      on.kind = on_day::DayOfMonth;
>       on.day_of_month = d;
>-      to = on;
>       return in;
>     }
>  }
>@@ -2226,7 +2227,6 @@ namespace std::chrono
>     {
>       on.kind = on_day::LastWeekday;
>       on.day_of_week = w.wd.c_encoding();
>-      to = on;
>       return in;
>     }
>  }
>@@ -2238,14 +2238,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;
>     }
>  }
>@@ -2255,6 +2254,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_day_spec(in, on))
>+ to = on;
>+      return in;
>+    }
>+
>     istream& operator>>(istream& in, at_time& at)
>     {
>       int sign = 1;
>@@ -2357,12 +2367,17 @@ 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_day on{.kind = on_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);
>+      if (!ws(in).eof())
>+ if (parse_day_spec(in, on))
>+  in >> t;

This could be:

     if (in >> on.day() >> t)

You could have written it like that already, because parse_day_spec
returns the stream, so:

     if (parse_day_spec(in, on) >> t)

but I think it's a lot more clear what's happening when written as two
chained operator>> calls.

There's not much benefit to returning istream& from parse_day_spec
because your patch only ever uses it as a bool anyway. But if we
replace it with another operator>> overload then it makes sense to use
it in chained expressions like this.

>+    }
>+  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..6b5ce6bae
>--- /dev/null
>+++ b/libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc
>@@ -0,0 +1,168 @@
>+// { 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 );

There should be the same XFAIL comment on this line above.

>+  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 );
>+  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 );
>+  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
>+)";
>+
>+  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;
>+
>+  // 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
>+)";
>+
>+  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



More information about the Libstdc++ mailing list