[PATCH v2 5/5] libstdc++: Implement zic writezone merge optimization [PR124854]

Tomasz Kamiński tkaminsk@redhat.com
Tue May 19 07:58:26 GMT 2026


Below including a quick draft of alternative solution where the merging
is integragted into pre-existing loop. We use merge_window (non-zero
only after DST chgange), and skip adding sys_info related to them.

Let me know what you think about these approach. I certainly like it
more.
---
 libstdc++-v3/src/c++20/tzdb.cc                | 58 ++++++++-----
 .../std/time/time_zone/zone_merge.cc          | 84 +++++++++++++++++++
 2 files changed, 122 insertions(+), 20 deletions(-)
 create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc

diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
index 183a0c61a61..e9a348cc3c8 100644
--- a/libstdc++-v3/src/c++20/tzdb.cc
+++ b/libstdc++-v3/src/c++20/tzdb.cc
@@ -28,6 +28,8 @@
 // In the usual dual-abi build, std::chrono::tzdb is only defined for cxx11.
 #define _GLIBCXX_USE_CXX11_ABI 1
 
+#define _GLIBCXX_ASSERTIONS
+
 #include <chrono>
 #include <fstream>    // ifstream
 #include <sstream>    // istringstream
@@ -78,6 +80,8 @@
 # endif
 #endif
 
+#include <iostream>
+
 namespace __gnu_cxx
 {
 #ifdef _AIX
@@ -504,6 +508,9 @@ namespace std::chrono
       seconds
       offset() const noexcept { return m_offset; }
 
+      minutes
+      save() const noexcept { return m_save; }
+
       // RULES: The name of the rules that apply for this period.
       string_view
       rules() const noexcept
@@ -867,8 +874,15 @@ namespace std::chrono
     info.abbrev = ri.format();
 
     string_view letters;
-    if (i != infos.begin() && i[-1].expanded())
-      letters = i[-1].next_letters();
+    seconds prev_offset = ri.offset();
+    if (i == infos.begin())
+      prev_offset = ri.offset();
+    else if (const ZoneInfo& prev = i[-1]; !prev.expanded())
+      prev_offset = prev.offset();
+    else if (letters = prev.next_letters(); letters.empty())
+      prev_offset = prev.offset() + prev.save();
+    else
+      prev_offset = ri.offset();
 
     if (letters.empty())
       {
@@ -940,9 +954,10 @@ namespace std::chrono
       }
 
     const Rule* curr_rule = nullptr;
-
+    seconds merge_window = (prev_offset > info.offset) ? (prev_offset - info.offset) : seconds(0);
     while (info.begin < info.end && num_after > 0)
       {
+
 	sys_seconds t = info.begin;
 	const year_month_day date(chrono::floor<days>(t));
 	const Rule* next_rule = nullptr;
@@ -984,9 +999,6 @@ namespace std::chrono
 
 	    if (t < rule_start && rule_start < info.end)
 	      {
-		if (rule_start - t < days(1)) // XXX shouldn't be needed!
-		  continue;
-
 		// Found a closer transition than the previous info.end.
 		info.end = rule_start;
 		next_rule = &rule;
@@ -995,7 +1007,6 @@ namespace std::chrono
 
 	format_abbrev_str(info, letters);
 
-	bool merged = false;
 #if 0
 	if (!new_infos.empty())
 	  {
@@ -1015,21 +1026,28 @@ namespace std::chrono
 	else
 	  letters = {};
 
-	if (!merged)
+	// Merge the rule with next one (full description goes here)
+	if (bool merged = (info.end - t <= merge_window))
+	  info.begin = t;
+	else
+	{       
 	  new_infos.emplace_back(info, letters);
+	  if (info.begin <= tp && tp < info.end) // Found the result.
+	    result_index = new_infos.size() - 1;
+	  else if (result_index >= 0)
+	    {
+	      // Finish on a DST sys_info if possible, so that if we resume
+	      // generating sys_info objects after this time point, save=0
+	      // should be correct for the next sys_info.
+	      if (num_after > 1 || info.save != 0min)
+	        --num_after;
+	    }
+	  info.begin = info.end;
+	}
 
-	if (info.begin <= tp && tp < info.end) // Found the result.
-	  result_index = new_infos.size() - 1;
-	else if (result_index >= 0 && !merged)
-	  {
-	    // Finish on a DST sys_info if possible, so that if we resume
-	    // generating sys_info objects after this time point, save=0
-	    // should be correct for the next sys_info.
-	    if (num_after > 1 || info.save != 0min)
-	      --num_after;
-	  }
-
-	info.begin = info.end;
+	// Merging applies only when switching between rules with different
+	// offset, for any later applications it should be zero.
+	merge_window = seconds(0);
 	if (next_rule)
 	  {
 	    info.end = ri.until();
diff --git a/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc b/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc
new file mode 100644
index 00000000000..75942f976b6
--- /dev/null
+++ b/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc
@@ -0,0 +1,84 @@
+// { 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 two adjacent Zone lines differ in total offset and the new line's
+// rule set has a rule firing within |jump| of the boundary (where jump
+// is a backward local-time jump), zic.c's writezone folds that rule
+// into the boundary, so the new line begins with the post-rule save.
+//
+// Mirrors America/Argentina/Buenos_Aires around 1999-10-03.
+
+#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;
+
+  // stdoff jumps from -3 to -4 at the same instant a save=1 rule fires.
+  // In the new (-4) frame the rule fires 1 hour after the boundary at
+  // UT 03:00, so the merge folds the rule into the boundary and the
+  // new line begins at offset=-3, save=1 (abbrev "-03").
+  std::ofstream("tzdata.zi") << R"(# version test_zone_merge
+R T 1999 o - O 3 0 1 -
+R T 2000 o - Mar 3 0 0 -
+Z Test/BA -3 -  %z  1999 O 3
+          -4 T  %z  2000 Mar 3
+          -3 -  %z
+)";
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used );
+  VERIFY( db.version == "test_zone_merge" );
+
+  auto* tz = locate_zone("Test/BA");
+
+  // The boundary is the wall UNTIL "1999 O 3" (default time 00:00)
+  // interpreted in the prior (-3) frame, i.e. UT 03:00 1999-10-03.
+  sys_seconds boundary{sys_days(1999y/October/3) + 3h};
+
+  auto before = tz->get_info(boundary - 1s);
+  VERIFY( before.offset == -3h );
+  VERIFY( before.save == 0min );
+  VERIFY( before.abbrev == "-03" );
+
+  // The new line's first sys_info already has save=1 from the merge,
+  // total offset -3h, abbrev "-03".
+  auto at_boundary = tz->get_info(boundary);
+  VERIFY( at_boundary.offset == -3h );
+  VERIFY( at_boundary.save == 60min );
+  VERIFY( at_boundary.abbrev == "-03" );
+
+  auto plus_30min = tz->get_info(boundary + 30min);
+  VERIFY( plus_30min.offset == -3h );
+  VERIFY( plus_30min.save == 60min );
+  VERIFY( plus_30min.abbrev == "-03" );
+
+  // Sanity: well after the boundary, still in the merged sys_info
+  // until the Mar 3 2000 transition.
+  auto winter = tz->get_info(sys_days(2000y/January/15));
+  VERIFY( winter.offset == -3h );
+  VERIFY( winter.save == 60min );
+  VERIFY( winter.abbrev == "-03" );
+
+  // After Mar 3 2000: line 2 ends, line 3 begins.  No DST rule fires
+  // at this boundary, so total offset reverts to -3h with save=0.
+  auto spring = tz->get_info(sys_days(2000y/April/15));
+  VERIFY( spring.offset == -3h );
+  VERIFY( spring.save == 0min );
+  VERIFY( spring.abbrev == "-03" );
+}
-- 
2.54.0



More information about the Libstdc++ mailing list