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
};
// 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,
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
// 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.
<< ' ' << 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;
}
};
- 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;
}
}
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;
}
}
{
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;
}
}
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;
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
--- /dev/null
+// { 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();
+}