[gcc r16-9372] libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]

Tomasz Kaminski via Gcc-cvs <[email protected]>
Newsgroups gmane.comp.gcc.cvs
Message-ID <[email protected]>
https://gcc.gnu.org/g:36a953bfe6402a804139224a27a118f6a0d767d7

commit r16-9372-g36a953bfe6402a804139224a27a118f6a0d767d7
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]>
    (cherry picked from commit 2b5e55eea69acbc6e46fa3463164e62bcc436e0d)

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 e33e8d593059..79aebf18ca3d 100644
--- a/libstdc++-v3/src/c++20/tzdb.cc
+++ b/libstdc++-v3/src/c++20/tzdb.cc
@@ -223,7 +223,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
 
@@ -312,7 +312,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,
@@ -376,7 +376,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
@@ -599,9 +607,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.
@@ -663,17 +671,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;
@@ -2404,22 +2412,25 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
       }
     };
 
-    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;
 	    }
 	}
@@ -2428,9 +2439,8 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
 	  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;
 	    }
 	}
@@ -2442,14 +2452,13 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
 	    {
 	      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;
 		    }
 		}
@@ -2459,6 +2468,17 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
       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;
@@ -2561,12 +2581,16 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
       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();
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.