[PATCH 1/5] libstdc++: Fix numeric save offset on Zone lines [PR 124851]
Alvaro Begue
alvaro.begue@gmail.com
Sat Apr 11 13:33:05 GMT 2026
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.
---
libstdc++-v3/src/c++20/tzdb.cc | 13 ++--
.../std/time/time_zone/numeric_save.cc | 66 +++++++++++++++++++
2 files changed, 75 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 eb6ac7d07..33217f0a9 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,9 @@ namespace std::chrono
m_buf += info.first.abbrev;
}
- // STDOFF: Seconds from UTC during standard time.
+ // STDOFF: Seconds from UTC during standard time. Always the
+ // standard offset only; the saved value (if any) is in m_save and
+ // is added back when reconstructing a sys_info via to().
seconds
offset() const noexcept { return m_offset; }
@@ -539,7 +542,9 @@ namespace std::chrono
return false;
info.end = until();
- info.offset = offset();
+ // m_offset is the standard offset only; add the saved value to
+ // reconstruct the total offset. See ZoneInfo's m_offset comment.
+ 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..d7b78f215
--- /dev/null
+++ b/libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc
@@ -0,0 +1,66 @@
+// { 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* } }
+
+// Regression test: when a Zone line specifies a numeric value as its
+// RULES field, that value is the constant DST save value for that zone
+// line. ZoneInfo::to() previously set sys_info::offset to the zone
+// line's STDOFF only, ignoring the parsed save. Per [time.zone.info.sys]
+// sys_info::offset is the *total* UTC offset (stdoff + save), so any
+// zone line with a non-zero numeric save reported the wrong offset.
+//
+// Mirrors Africa/Gaborone's tzdata, which uses
+// 2 - CAT 1943 S 19 2
+// 2 1 CAST 1944 Mar 19 2 <-- numeric "1" RULES, save = +1h
+// 2 - CAT
+// The middle line is what triggers the bug.
+
+#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 );
+ 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