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

Reply via email to