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();
+}

Reply via email to