[PATCH v2 1/5] libstdc++: Fix numeric save offset on Zone lines [PR124851]

Jonathan Wakely jwakely@redhat.com
Mon May 11 16:40:07 GMT 2026


On Sun, 26 Apr 2026 at 19:51 -0400, Álvaro Begué wrote:
>When a Zone line specifies a numeric value as its RULES field (the
>constant DST save value for that zone line, e.g. Africa/Gaborone's
>"2 1 CAST" line), the parser stored the standard offset alone in
>ZoneInfo::m_offset. ZoneInfo::to() then returned that as
>sys_info::offset, dropping the numeric save and reporting a total
>offset that was wrong by the save amount.
>
>This was inconsistent with the two ZoneInfo constructors that take a
>sys_info, which previously stored the *total* offset (stdoff + save) in
>m_offset. As a result m_offset's semantics depended on which code path
>created the ZoneInfo, and only the parser path's lines with non-zero
>numeric save were observably broken.
>
>Fix by giving m_offset a single semantics: always the standard offset
>only. The two sys_info-taking constructors now subtract the save before
>storing, and to() adds it back when reconstructing the sys_info.
>
>The remaining .offset() callers inside _M_get_sys_info already expect
>the standard offset (they are computing rule firing times, where the
>save component is added separately from the active rule's save value),
>so no other call sites need adjustment.
>
>libstdc++-v3/ChangeLog:
>
>PR libstdc++/124851
>* src/c++20/tzdb.cc (ZoneInfo::ZoneInfo(sys_info&&)): Store
>stdoff only in m_offset (subtract info.save).
>(ZoneInfo::ZoneInfo(const pair<sys_info, string_view>&)):
>Likewise.
>(ZoneInfo::offset()): Document new semantics.
>(ZoneInfo::to(sys_info&)): Add m_save back to offset() when
>populating sys_info::offset.
>* testsuite/std/time/time_zone/numeric_save.cc: New test.
>
>Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>
>---
> libstdc++-v3/src/c++20/tzdb.cc                |  9 +--
> .../std/time/time_zone/numeric_save.cc        | 58 +++++++++++++++++++
> 2 files changed, 63 insertions(+), 4 deletions(-)
> create mode 100644
>libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
>
>diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
>index b0fbfc46a..1e49bb749 100644
>--- a/libstdc++-v3/src/c++20/tzdb.cc
>+++ b/libstdc++-v3/src/c++20/tzdb.cc
>@@ -478,11 +478,12 @@ namespace std::chrono
>
>       ZoneInfo(sys_info&& info)
>       : m_buf(std::move(info.abbrev)), m_expanded(true), m_save(info.save),
>- m_offset(info.offset), m_until(info.end)
>+ m_offset(info.offset - seconds(info.save)), m_until(info.end)
>       { }
>
>       ZoneInfo(const pair<sys_info, string_view>& info)
>-      : m_expanded(true), m_save(info.first.save),
>m_offset(info.first.offset),
>+      : m_expanded(true), m_save(info.first.save),
>+ m_offset(info.first.offset - seconds(info.first.save)),
>  m_until(info.first.end)
>       {
>  if (info.second.size())
>@@ -494,7 +495,7 @@ namespace std::chrono
>  m_buf += info.first.abbrev;
>       }
>
>-      // STDOFF: Seconds from UTC during standard time.
>+      // STDOFF: Seconds from UTC during standard time (without any save).
>       seconds
>       offset() const noexcept { return m_offset; }
>
>@@ -539,7 +540,7 @@ namespace std::chrono
>   return false;
>
>  info.end = until();
>- info.offset = offset();
>+ info.offset = offset() + seconds(m_save);
>  info.save = minutes(m_save);
>  info.abbrev = format();
>  format_abbrev_str(info); // expand %z
>diff --git a/libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
>b/libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
>new file mode 100644
>index 000000000..1b5b22263
>--- /dev/null
>+++ b/libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
>@@ -0,0 +1,58 @@
>+// { 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* } }
>+
>+// When a Zone line specifies a numeric value as its RULES field, that
>+// value is the constant DST save value for that zone line.  Per
>+// [time.zone.info.sys] sys_info::offset is the total UTC offset
>+// (stdoff + save).
>+
>+#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 "./";
>+  }
>+}
>+
>+int
>+main()
>+{
>+  using namespace std::chrono;
>+
>+  std::ofstream("tzdata.zi") << R"(# version test_numeric_save
>+Z Test/Gaborone 2 -  CAT  1943 Sep 19 2
>+                2 1  CAST 1944 Mar 19 2
>+                2 -  CAT
>+)";
>+
>+  const auto& db = reload_tzdb();
>+  VERIFY( override_used );

Please add a comment on the line above:

   VERIFY( override_used ); // If this fails then XFAIL for the target.

OK for trunk with that change, thanks.


>+  VERIFY( db.version == "test_numeric_save" );
>+
>+  auto* tz = locate_zone("Test/Gaborone");
>+
>+  // Sample well inside the CAST (numeric-save) zone line.
>+  auto info = tz->get_info(sys_days(1943y/December/15));
>+  VERIFY( info.offset == 3h );        // stdoff +2h + save +1h
>+  VERIFY( info.save == 60min );
>+  VERIFY( info.abbrev == "CAST" );
>+
>+  // Bordering zone lines should report the standard offset with save 0.
>+  auto before = tz->get_info(sys_days(1943y/September/1));
>+  VERIFY( before.offset == 2h );
>+  VERIFY( before.save == 0min );
>+  VERIFY( before.abbrev == "CAT" );
>+
>+  auto after = tz->get_info(sys_days(1944y/April/15));
>+  VERIFY( after.offset == 2h );
>+  VERIFY( after.save == 0min );
>+  VERIFY( after.abbrev == "CAT" );
>+}
>-- 
>2.34.1



More information about the Libstdc++ mailing list