[PATCH v2] libstdc++: Make chrono::tzdb parser case-insensive [PR123510]
Jonathan Wakely
jwakely@redhat.com
Thu Aug 27 12:04:57 GMT 2026
On Thu, 27 Aug 2026 at 11:35, Tomasz Kamiński <tkaminsk@redhat.com> wrote:
>
> Expand switch and if-check to check both lowercase and uppercase letter.
> For line parsing in chrono::reload_tzdb, the type is inspected by default
> branch (check for 'Z'), this patch normalizes it for all branches
> consistently.
>
> For testing, we check minimum unambigous prefix and whole world as both
> lowercase and uppercase. This covers all code paths.
>
> libstdc++-v3/ChangeLog:
>
> PR libstdc++/123510
> * src/c++20/tzdb.cc (at_time::is_indicator): Add case labels
> for uppercase letters.
> (operator>>(istream&, years_from_to): Check for 'M' and 'O',
> (chrono::reload_tzdb): Add case labels for lowercase letters,
> and normalize first letter of type.
> (operator>>(istream&, abbrev_month&): Match all cases in switch
> and days_chars.
> (operator>>(istream&, on_month_day&)): Check for 'L'.
> * testsuite/std/time/tzdb/name_matching.cc: New test.
> ---
> v2:
> * explains why type is updated in commit message
> * reduces test cases to prefix/word upper/lower case, the time of test
> exacutable is 0.006 on my machine
Ah I think I included the time to run dejagnu and compile the test ...
so 6s is not a fair number.
v2 is OK for trunk and gcc-16, thanks.
> * for years/line simply list four combinations, instead of generating
> 4 for each argument
> * changed switch back to if in abbrev_month matching
> * added uppercase letters after lower case letters in days_chars
>
> libstdc++-v3/src/c++20/tzdb.cc | 52 ++-
> .../testsuite/std/time/tzdb/name_matching.cc | 318 ++++++++++++++++++
> 2 files changed, 364 insertions(+), 6 deletions(-)
> create mode 100644 libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc
>
> diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
> index 0d004a8a6fe..0ffb3c014d1 100644
> --- a/libstdc++-v3/src/c++20/tzdb.cc
> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> @@ -272,14 +272,20 @@ namespace std::chrono
> switch (c)
> {
> case 's':
> + case 'S':
> return {Standard, true};
> case 'u':
> + case 'U':
> case 'g':
> + case 'G':
> case 'z':
> + case 'Z':
> return {Universal, true};
> case 'w':
> + case 'W':
> return {Wall, true};
> case 'd':
> + case 'D':
> return {Daylight, true};
> default:
> return {Wall, false};
> @@ -399,7 +405,7 @@ namespace std::chrono
> {
> string s;
> auto c = ws(in).peek();
> - if (c == 'm') [[unlikely]] // keyword "minimum"
> + if (c == 'm' || c == 'M') [[unlikely]] // keyword "minimum"
> {
> in >> s; // extract the rest of the word
> yy.from = year(1900);
> @@ -408,12 +414,12 @@ namespace std::chrono
> yy.from = year{num};
>
> c = ws(in).peek();
> - if (c == 'm') // keyword "maximum"
> + if (c == 'm' || c == 'M') // keyword "maximum"
> {
> in >> s; // extract the rest of the word
> yy.to = year::max();
> }
> - else if (c == 'o') // keyword "only"
> + else if (c == 'o' || c == 'O') // keyword "only"
> {
> in >> s; // extract the rest of the word
> yy.to = yy.from;
> @@ -1969,28 +1975,37 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
> {
> case '#':
> break;
> + case 'r':
> case 'R':
> {
> // Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S
> is >> type; // extract the "Rule" or "R" marker
> + type[0] = 'R';
> +
> Rule rule;
> is >> rule;
> node->rules.push_back(std::move(rule));
> break;
> }
> + case 'l':
> case 'L':
> {
> // Link TARGET LINK-NAME
> is >> type; // extract the "Link" or "L" marker
> + type[0] = 'L';
> +
> time_zone_link link(nullptr);
> is >> quoted(link._M_target) >> quoted(link._M_name);
> node->db.links.push_back(std::move(link));
> break;
> }
> + case 'z':
> case 'Z':
> {
> // Zone NAME STDOFF RULES FORMAT [UNTIL]
> is >> type; // extract the "Zone" or "Z" marker
> + type[0] = 'Z';
> +
> time_zone tz(std::make_unique<time_zone::_Impl>(node));
> is >> quoted(tz._M_name);
> node->db.zones.push_back(time_zone(std::move(tz)));
> @@ -2459,60 +2474,76 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
> in >> s;
> switch (s[0])
> {
> + case 'j':
> case 'J':
> switch (s[1])
> {
> case 'a':
> + case 'A':
> am.m = January;
> return in;
> case 'u':
> + case 'U':
> switch (s[2])
> {
> case 'n':
> + case 'N':
> am.m = June;
> return in;
> case 'l':
> + case 'L':
> am.m = July;
> return in;
> }
> break;
> }
> break;
> + case 'f':
> case 'F':
> am.m = February;
> return in;
> + case 'm':
> case 'M':
> - if (s[1] == 'a') [[likely]]
> + if (s[1] == 'a' || s[1] == 'A') [[likely]]
> switch (s[2])
> {
> case 'r':
> + case 'R':
> am.m = March;
> return in;
> case 'y':
> + case 'Y':
> am.m = May;
> return in;
> }
> break;
> + case 'a':
> case 'A':
> switch (s[1])
> {
> case 'p':
> + case 'P':
> am.m = April;
> return in;
> case 'u':
> + case 'U':
> am.m = August;
> return in;
> }
> break;
> + case 's':
> case 'S':
> am.m = September;
> return in;
> + case 'o':
> case 'O':
> am.m = October;
> return in;
> + case 'n':
> case 'N':
> am.m = November;
> return in;
> + case 'd':
> case 'D':
> am.m = December;
> return in;
> @@ -2536,37 +2567,46 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
> // Just peek at one char at a time.
> switch (in.peek())
> {
> + case 'm':
> case 'M':
> aw.wd = Monday;
> break;
> + case 't':
> case 'T':
> in.ignore(1); // Discard the 'T'
> switch (in.peek())
> {
> case 'u':
> + case 'U':
> aw.wd = Tuesday;
> break;
> case 'h':
> + case 'H':
> aw.wd = Thursday;
> break;
> default:
> in.setstate(ios::failbit);
> }
> break;
> + case 'w':
> case 'W':
> aw.wd = Wednesday;
> break;
> + case 'f':
> case 'F':
> aw.wd = Friday;
> break;
> + case 's':
> case 'S':
> in.ignore(1); // Discard the 'S'
> switch (in.peek())
> {
> case 'a':
> + case 'A':
> aw.wd = Saturday;
> break;
> case 'u':
> + case 'U':
> aw.wd = Sunday;
> break;
> default:
> @@ -2579,7 +2619,7 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
> in.ignore(1); // Discard whichever char we just looked at.
>
> // Discard any remaining chars from weekday, e.g. "onday".
> - string_view day_chars = "ondayesritu";
> + string_view day_chars = "ondayesrituONDAYESRITU";
> auto is_day_char = [&day_chars](int c) {
> return c != char_traits<char>::eof()
> && day_chars.find((char)c) != day_chars.npos;
> @@ -2613,7 +2653,7 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
> return in;
> }
> }
> - else if (c == 'l') // lastSunday, lastWed, ...
> + else if (c == 'l' || c == 'L') // lastSunday, lastWed, ...
> {
> in.ignore(4);
> if (abbrev_weekday w{}; in >> w) [[likely]]
> diff --git a/libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc b/libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc
> new file mode 100644
> index 00000000000..4c70e986a01
> --- /dev/null
> +++ b/libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc
> @@ -0,0 +1,318 @@
> +// { 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* } }
> +
> +#include <chrono>
> +#include <fstream>
> +#include <format>
> +#include <testsuite_hooks.h>
> +#include <iostream>
> +
> +static bool override_used = false;
> +
> +namespace __gnu_cxx
> +{
> + const char* zoneinfo_dir_override() {
> + override_used = true;
> + return "./";
> + }
> +}
> +
> +using namespace std::chrono;
> +
> +template<typename Functor, typename... Args>
> + void
> + test_combinations(Functor&& func, std::string_view txt, size_t min, Args&&... args)
> + {
> + std::string lower(txt);
> + std::string upper(txt);
> + for (char& c : upper)
> + c = std::toupper(c);
> +
> + func(lower.substr(0, min), args...);
> + func(upper.substr(0, min), args...);
> + func(lower, args...);
> + func(upper, args...);
> + }
> +
> +void
> +test_day(std::string_view name, weekday wd)
> +{
> + constexpr const char* templ = R"(# version test_day_{0}
> + # Zone NAME STDOFF RULES FORMAT [UNTIL]
> + Zone Test 0 - Z1 2000 May last{0}
> + 0 - Z2 2010 May {0}>=10
> + 0 - Z3
> + )";
> +
> + std::string input = std::format(templ, name);
> + std::ofstream("tzdata.zi") << input;
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> +
> + std::string_view ver = db.version;
> + VERIFY( ver.starts_with("test_day_") );
> + ver.remove_prefix(9);
> + VERIFY( ver == name );
> +
> + sys_info info = locate_zone("Test")->get_info(sys_days(2005y/January/1));
> + VERIFY( info.begin == sys_days(2000y/May/wd[last]) );
> + year_month_day ymd(ceil<days>(info.end));
> + VERIFY( ymd.year() == 2010y );
> + VERIFY( ymd.month() == May );
> + VERIFY( ymd.day() >= day(10) );
> + VERIFY( weekday(ymd) == wd );
> +}
> +
> +void
> +test_last(std::string_view name)
> +{
> + constexpr const char* templ = R"(# version test_{0}
> + # Zone NAME STDOFF RULES FORMAT [UNTIL]
> + Zone Test 0 - Z1 2000 July {0}Mon
> + 0 - Z3
> + )";
> +
> + std::string input = std::format(templ, name);
> + std::ofstream("tzdata.zi") << input;
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> +
> + std::string_view ver = db.version;
> + VERIFY( ver.starts_with("test_") );
> + ver.remove_prefix(5);
> + VERIFY( ver == name );
> +
> + sys_info info = locate_zone("Test")->get_info(sys_days(2005y/January/1));
> + VERIFY( info.begin == sys_days(2000y/July/Monday[last]) );
> +}
> +
> +void
> +test_days()
> +{
> + test_combinations(test_day, "monday", 1, Monday);
> + test_combinations(test_day, "tuesday", 2, Tuesday);
> + test_combinations(test_day, "wednesday", 1, Wednesday);
> + test_combinations(test_day, "thursday", 2, Thursday);
> + test_combinations(test_day, "friday", 1, Friday);
> + test_combinations(test_day, "saturday", 2, Saturday);
> + test_combinations(test_day, "sunday", 2, Sunday);
> +
> + test_last("last");
> + test_last("LAST");
> +}
> +
> +void
> +test_month(std::string_view name, month m)
> +{
> + constexpr const char* templ = R"(# version test_month_{0}
> + # Zone NAME STDOFF RULES FORMAT [UNTIL]
> + Zone Test 0 - Z1 2000 {0} 13
> + 0 - Z2 2010 {0}
> + 0 - Z3
> + )";
> +
> + std::string input = std::format(templ, name);
> + std::ofstream("tzdata.zi") << input;
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> +
> + std::string_view ver = db.version;
> + VERIFY( ver.starts_with("test_month_") );
> + ver.remove_prefix(11);
> + VERIFY( ver == name );
> +
> + sys_info info = locate_zone("Test")->get_info(sys_days(2005y/January/1));
> + VERIFY( info.begin == sys_days(2000y/m/13) );
> + VERIFY( info.end == sys_days(2010y/m/1) );
> +}
> +
> +void
> +test_months()
> +{
> + test_combinations(test_month, "january", 2, January);
> + test_combinations(test_month, "february", 1, February);
> + test_combinations(test_month, "march", 3, March);
> + test_combinations(test_month, "april", 2, April);
> + test_combinations(test_month, "may", 3, May);
> + test_combinations(test_month, "june", 3, June);
> + test_combinations(test_month, "july", 3, July);
> + test_combinations(test_month, "august", 3, August);
> + test_combinations(test_month, "september", 1, September);
> + test_combinations(test_month, "october", 1, October);
> + test_combinations(test_month, "november", 1, November);
> + test_combinations(test_month, "december", 1, December);
> +}
> +
> +void
> +test_year(std::string_view min, std::string_view max, std::string_view only)
> +{
> + constexpr const char* templ = R"(# version {0}
> + # Rule NAME FROM TO - IN ON AT SAVE LETTER/S
> + Rule Rule {1} 2020 - Jan 10 0u 0 S
> + Rule Rule 2000 {2} - Nov 12 0u 1 D
> + Rule Rule 2010 {3} - Aug 11 0u 2 O
> + Rule Rule 2020 {2} - Jan 13 0u 0 S
> + # Zone NAME STDOFF RULES FORMAT [UNTIL]
> + Zone Test 0 - Z1 1800 Jan 1 0u
> + 0 Rule Z%s 3001 Jan 1 0u
> + 0 - Zl
> + )";
> +
> + const std::string ver = std::format("test_{}_{}_{}", min, max, only);
> + std::string input = std::format(templ, ver, min, max, only);
> + std::ofstream("tzdata.zi") << input;
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> + VERIFY( db.version == ver );
> +
> + const time_zone* zone = locate_zone("Test");
> +
> + // min applies since 1900, check first transition
> + sys_info info = zone->get_info(sys_days(1900y/January/1));
> + VERIFY( info.begin == sys_days(1800y/January/1) );
> + VERIFY( info.end == sys_days(1900y/January/10) );
> +
> + // Check 2010 only rule does not apply in 2009
> + info = zone->get_info(sys_days(2009y/February/1));
> + VERIFY( info.begin == sys_days(2009y/January/10) );
> + VERIFY( info.end == sys_days(2009y/November/12) );
> +
> + // Check 2010 only rule in 2010
> + info = zone->get_info(sys_days(2010y/February/1));
> + VERIFY( info.begin == sys_days(2010y/January/10) );
> + VERIFY( info.end == sys_days(2010y/August/11) );
> +
> + info = zone->get_info(sys_days(2010y/October/1));
> + VERIFY( info.begin == sys_days(2010y/August/11) );
> + VERIFY( info.end == sys_days(2010y/November/12) );
> +
> + // Check 2010 only rule does not apply in 2011
> + info = zone->get_info(sys_days(2011y/February/1));
> + VERIFY( info.begin == sys_days(2011y/January/10) );
> + VERIFY( info.end == sys_days(2011y/November/12) );
> +
> + // max rule applies foreve
> + info = zone->get_info(sys_days(2500y/February/1));
> + VERIFY( info.begin == sys_days(2500y/January/13) );
> + VERIFY( info.end == sys_days(2500y/November/12) );
> +
> + info = zone->get_info(sys_days(3000y/December/1));
> + VERIFY( info.begin == sys_days(3000y/November/12) );
> + VERIFY( info.end == sys_days(3001y/January/1) );
> +}
> +
> +void
> +test_years()
> +{
> + test_year("min", "max", "only");
> + test_year("m", "m", "o");
> + test_year("MIN", "MAX", "ONLY");
> + test_year("M", "M", "O");
> +}
> +
> +void
> +test_time(char s, char u, char w, char d)
> +{
> + constexpr const char* templ = R"(# version test_time_{0}{1}{2}{3}
> + # Zone NAME STDOFF RULES FORMAT [UNTIL]
> + Zone Test 1 0 Zs 2000 Jan 11 10{0}
> + 3 1 Zu 2005 Feb 12 11{1}
> + 5 1 Zw 2010 Mar 13 12{2}
> + 7 1 Zd 2015 Apr 14 13{3}
> + 9 0 Su 2020 May 15 14{1}
> + 10 0 Sw 2025 Jun 16 15{2}
> + 11 - Zl
> + )";
> +
> + std::string input = std::format(templ, s, u, w, d);
> + std::ofstream("tzdata.zi") << input;
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> +
> + std::string_view ver = db.version;
> + VERIFY( ver.starts_with("test_time_") );
> + VERIFY( ver[10] == s );
> + VERIFY( ver[11] == u );
> + VERIFY( ver[12] == w );
> + VERIFY( ver[13] == d );
> +
> + const time_zone* zone = locate_zone("Test");
> + sys_info info = zone->get_info(sys_days(2002y/January/1));
> + VERIFY( info.begin == sys_days(2000y/January/11) + 10h - 1h );
> + VERIFY( info.end == sys_days(2005y/February/12) + 11h );
> +
> + info = zone->get_info(sys_days(2012y/January/1));
> + VERIFY( info.begin == sys_days(2010y/March/13) + 12h - 6h );
> + VERIFY( info.end == sys_days(2015y/April/14) + 13h - 8h );
> +
> + info = zone->get_info(sys_days(2022y/January/1));
> + VERIFY( info.begin == sys_days(2020y/May/15) + 14h );
> + VERIFY( info.end == sys_days(2025y/June/16) + 15h - 10h );
> +}
> +
> +void
> +test_times()
> +{
> + test_time('s', 'u', 'w', 'd');
> + test_time('S', 'U', 'W', 'D');
> + // Alternate spelling for Universal
> + test_time('s', 'g', 'w', 'd');
> + test_time('s', 'G', 'w', 'd');
> + test_time('s', 'z', 'w', 'd');
> + test_time('s', 'Z', 'w', 'd');
> +}
> +
> +void
> +test_line(std::string_view rule, std::string_view zone, std::string_view link)
> +{
> + constexpr const char* templ = R"(# version {0}
> + # Rule NAME FROM TO - IN ON AT SAVE LETTER/S
> + {1} Rule min max - Jan 13 0u 0 D
> + {1} Rule min max - Oct 12 0u 1 S
> + # Zone NAME STDOFF RULES FORMAT [UNTIL]
> + {2} Test 0 - Z1 2000 Jan 13
> + 0 Rule Z%s
> + # Link TARGET LINK-NAME
> + {3} Test Link
> + )";
> +
> + const std::string ver = std::format("test_{}_{}_{}", rule, zone, link);
> + std::string input = std::format(templ, ver, rule, zone, link);
> + std::ofstream("tzdata.zi") << input;
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> + VERIFY( db.version == ver );
> +
> + sys_info info = locate_zone("Test")->get_info(sys_days(2005y/February/1));
> + VERIFY( info.begin == sys_days(2005y/January/13) );
> + VERIFY( info.end == sys_days(2005y/October/12) );
> +
> + const time_zone* target = locate_zone("Link");
> + VERIFY( target->name() == "Test" );
> +}
> +
> +void
> +test_lines()
> +{
> + test_line("rule", "zone", "link");
> + test_line("r", "z", "l");
> + test_line("RULE", "ZONE", "LINK");
> + test_line("R", "Z", "L");
> +}
> +
> +int main()
> +{
> + test_days();
> + test_months();
> + test_years();
> + test_times();
> + test_lines();
> +}
> --
> 2.55.0
>
More information about the Libstdc++
mailing list