Attaching test harness.
On Fri, Jul 17, 2026 at 1:46 PM Tomasz Kamiński <[email protected]> wrote:
> As when member of Transitions::Entries that do not correspond
> to rule activation (rule is empty) is either minimum or maximum
> value of sys_seconds, adjusting it to std_offset lead to overflow
> (and UB). In practice this caused next.when to have very low value,
> and thus considered to be active rule. The to_local conversion now
> return local_seconds constucted directly from time_since_epoch
> (i.e. minimum and maximum).
>
> In consequence above, the save was still not applied for zones
> that specified UNTIL after firing of last rule, that aslo have
> non zero save (e.g. Asia/Bishkek 2005 transition).
>
> libstdc++-v3/ChangeLog:
>
> PR libstdc++/116110
> * src/c++20/tzdb.cc (find_active_rule::to_local): Do not
> modify min/max values for rules.
> * testsuite/std/time/time_zone/wall_cascade.cc: Add test
> for last_transition.
> ---
> I haven't noticed this at first, because this was not causing
> any regression (the save was not applied before). Found that
> while analysing output of the comparision.
>
> For output comparision I am using a slightly different program
> than your. It quries sys_info object for give time from
> both libstdc++ and date, and compares abbrev, offset and save.
> Then I move to time point at the end of sys_info that finished
> first, and perform check again.
>
> The above does not fail if the time range of same offset, save
> and abbrev is split differently between libstdc++ and date.
> (I still emit a WARN on that, but I do not think that fixing
> them is necessary).
>
> Testing on x86_64-linux. *time_zone* test passed.
> OK for trunk?
>
> libstdc++-v3/src/c++20/tzdb.cc | 13 ++--
> .../std/time/time_zone/wall_cascade.cc | 73 +++++++++++++++++++
> 2 files changed, 81 insertions(+), 5 deletions(-)
>
> diff --git a/libstdc++-v3/src/c++20/tzdb.cc
> b/libstdc++-v3/src/c++20/tzdb.cc
> index e2d607dccb9..460a242fed1 100644
> --- a/libstdc++-v3/src/c++20/tzdb.cc
> +++ b/libstdc++-v3/src/c++20/tzdb.cc
> @@ -853,12 +853,15 @@ namespace std::chrono
>
> auto to_local = [&](const Transitions::Entry& tran, const Rule*
> before)
> {
> - local_seconds ls(tran.when.time_since_epoch() + std_offset);
> - if (!before || !tran.rule)
> + local_seconds ls(tran.when.time_since_epoch());
> + if (!tran.rule)
> + // `when` is either min() or max(), and applying std_offset
> overflows.
> return ls;
> - if (tran.rule->when.indicator == at_time::Wall)
> - // 'when' was converted from local time without considering
> - // running 'save' in first place
> +
> + ls += std_offset;
> + if (!before || tran.rule->when.indicator == at_time::Wall)
> + // no active rule or 'when' was converted from local time without
> + // considering running 'save' in first place
> return ls;
> return ls + before->save;
> };
> diff --git a/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
> b/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
> index 3529c2de852..f9167b79612 100644
> --- a/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
> +++ b/libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc
> @@ -261,6 +261,78 @@ Z Europe/Lisbon -0:36:45 - LMT 1884
> VERIFY( at_boundary.abbrev == "WET" );
> }
>
> +void
> +test_last_transition()
> +{
> + std::ofstream("tzdata.zi") << R"(# version test_bishkek
> +R R 1984 1995 - S lastSu 2s 0 -
> +R R 1985 2010 - Mar lastSu 2s 1 S
> +R R 1996 2010 - O lastSu 2s 0 -
> +R KG 1992 1996 - Ap Su>=7 0s 1 -
> +R KG 1992 1996 - S lastSu 0 0 -
> +R KG 1997 2005 - Mar lastSu 2:30 1 -
> +R KG 1997 2004 - O lastSu 2:30 0 -
> +Z Test/Bishkek 4:58:24 - LMT 1924 May 2
> +5 R %z 1991 Au 31 2
> +5 KG %z 2005 Au 12
> + )";
> +
> + const auto& db = reload_tzdb();
> + VERIFY( override_used ); // If this fails then XFAIL for the target.
> + VERIFY( db.version == "test_bishkek" );
> +
> + auto* tz = locate_zone("Test/Bishkek");
> +
> + sys_seconds transitions[]{
> + sys_seconds{sys_days{1991y/August/30}} + 20h, // from 5 R %z 1991 Au
> 31 2
> + sys_seconds{sys_days{1992y/April/11}} + 19h,
> + sys_seconds{sys_days{1992y/September/26}} + 18h,
> + sys_seconds{sys_days{1993y/April/10}} + 19h,
> + sys_seconds{sys_days{1993y/September/25}} + 18h,
> + sys_seconds{sys_days{1994y/April/9}} + 19h,
> + sys_seconds{sys_days{1994y/September/24}} + 18h,
> + sys_seconds{sys_days{1995y/April/8}} + 19h,
> + sys_seconds{sys_days{1995y/September/23}} + 18h,
> + sys_seconds{sys_days{1996y/April/6}} + 19h,
> + sys_seconds{sys_days{1996y/September/28}} + 18h,
> + sys_seconds{sys_days{1997y/March/29}} + 21h + 30min,
> + sys_seconds{sys_days{1997y/October/25}} + 20h + 30min,
> + sys_seconds{sys_days{1998y/March/28}} + 21h + 30min,
> + sys_seconds{sys_days{1998y/October/24}} + 20h + 30min,
> + sys_seconds{sys_days{1999y/March/27}} + 21h + 30min,
> + sys_seconds{sys_days{1999y/October/30}} + 20h + 30min,
> + sys_seconds{sys_days{2000y/March/25}} + 21h + 30min,
> + sys_seconds{sys_days{2000y/October/28}} + 20h + 30min,
> + sys_seconds{sys_days{2001y/March/24}} + 21h + 30min,
> + sys_seconds{sys_days{2001y/October/27}} + 20h + 30min,
> + sys_seconds{sys_days{2002y/March/30}} + 21h + 30min,
> + sys_seconds{sys_days{2002y/October/26}} + 20h + 30min,
> + sys_seconds{sys_days{2003y/March/29}} + 21h + 30min,
> + sys_seconds{sys_days{2003y/October/25}} + 20h + 30min,
> + sys_seconds{sys_days{2004y/March/27}} + 21h + 30min,
> + sys_seconds{sys_days{2004y/October/30}} + 20h + 30min,
> + sys_seconds{sys_days{2005y/March/26}} + 21h + 30min,
> + sys_seconds{sys_days{2005y/August/11}} + 18h, // 5 KG %z 2005 Au 12
> + };
> +
> + for (size_t i = 1; i < std::size(transitions); ++i)
> + {
> + const minutes save = (i % 2) ? 0h : 1h;
> + const sys_seconds t = transitions[i-1];
> + const sys_info info = tz->get_info(t);
> + VERIFY( info.begin == t );
> + VERIFY( info.offset == 5h + save );
> + VERIFY( info.save == save );
> + VERIFY( info.end == transitions[i] );
> + }
> +
> + // The transition 2005 Au 12 has total offset 6 (5h + 1h).
> + sys_seconds t = transitions[std::size(transitions)-1];
> + const sys_info info = tz->get_info(t);
> + VERIFY( info.offset == 6h );
> + VERIFY( info.save == 1h );
> +}
> +
> int
> main()
> {
> @@ -270,4 +342,5 @@ main()
> test_prev_year();
> test_earlier_year();
> test_at_boundary();
> + test_last_transition();
> }
> --
> 2.55.0
>
>
#include <date/date.h>
#include <date/tz.h>
#include <iostream>
#include <chrono>
#include <cassert>
#define VERIFY assert
using namespace date::literals;
using namespace std::literals::chrono_literals;
using std::chrono::hours;
using std::chrono::seconds;
namespace chrono = std::chrono;
std::string cmp(date::sys_info const& di, chrono::sys_info const& ci)
{
std::ostringstream out;
if (di.abbrev != ci.abbrev)
out << "abbrev: " << di.abbrev << " != " << ci.abbrev << '\n';
if (di.offset != ci.offset)
out << "offset: " << di.offset << " != " << ci.offset << '\n';
if ((di.save != 0min) != (ci.save != 0min))
out << "save: " << di.save << " != " << ci.save << '\n';
return std::move(out).str();
}
int main(int argc, char** argv)
{
std::string_view zone = "Egypt";
int start_year = -5000;
if (argc > 1)
zone = argv[1];
if (argc > 2)
start_year = std::stoi(argv[2]);
int num_years = +2037 - start_year - 1;
if (argc > 3)
num_years = std::stoi(argv[3]);
date::sys_time<seconds> dt(date::sys_days(date::year(start_year)/date::January/1));
const date::time_zone* dtz = date::locate_zone(zone);
chrono::sys_time<seconds> ct(chrono::sys_days(chrono::year(start_year)/chrono::January/1));
const chrono::time_zone* ctz = chrono::locate_zone(zone);
const auto end = ct + chrono::years(num_years);
std::cout << "** ZONE # " << zone << "\n";
while (ct < end)
{
date::sys_info di = dtz->get_info(dt);
chrono::sys_info ci = ctz->get_info(ct);
if (auto res = cmp(di, ci); !res.empty())
{
std::cout << "FAIL @" << ct << ": " << ci << "\n";
std::cout << res;
}
else if (di.end != ci.end)
{
std::cout << "WARN @" << ct << ": " << ci << "\n";
std::cout << "end: " << di.end << " != " << ci.end << '\n';
}
else
std::cout << "PASS @" << ct << ": " << ci << "\n";
auto dd = di.end.time_since_epoch();
auto cd = ci.end.time_since_epoch();
auto min = std::min(dd, cd);
if (min <= ct.time_since_epoch())
break;
dt = date::sys_seconds(min);
ct = chrono::sys_seconds(min);
}
}