{"id":2222414,"url":"http://patchwork.ozlabs.org/api/1.2/patches/2222414/?format=json","web_url":"http://patchwork.ozlabs.org/project/gcc/patch/c2e2e6d472dd8842a2fff5fa7c85634a5c19f08e.1775912642.git.alvaro.begue@gmail.com/","project":{"id":17,"url":"http://patchwork.ozlabs.org/api/1.2/projects/17/?format=json","name":"GNU Compiler Collection","link_name":"gcc","list_id":"gcc-patches.gcc.gnu.org","list_email":"gcc-patches@gcc.gnu.org","web_url":null,"scm_url":null,"webscm_url":null,"list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<c2e2e6d472dd8842a2fff5fa7c85634a5c19f08e.1775912642.git.alvaro.begue@gmail.com>","list_archive_url":null,"date":"2026-04-11T13:33:09","name":"[5/5] libstdc++: Implement zic writezone merge optimization [PR 124854]","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"40bee73770898cba5df9c3c26810861d52c1cf3b","submitter":{"id":93119,"url":"http://patchwork.ozlabs.org/api/1.2/people/93119/?format=json","name":"Alvaro Begue","email":"alvaro.begue@gmail.com"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/gcc/patch/c2e2e6d472dd8842a2fff5fa7c85634a5c19f08e.1775912642.git.alvaro.begue@gmail.com/mbox/","series":[{"id":499557,"url":"http://patchwork.ozlabs.org/api/1.2/series/499557/?format=json","web_url":"http://patchwork.ozlabs.org/project/gcc/list/?series=499557","date":"2026-04-11T13:33:04","name":"libstdc++: chrono tzdb correctness fixes","version":1,"mbox":"http://patchwork.ozlabs.org/series/499557/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/2222414/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2222414/checks/","tags":{},"related":[],"headers":{"Return-Path":"<gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org>","X-Original-To":["incoming@patchwork.ozlabs.org","gcc-patches@gcc.gnu.org"],"Delivered-To":["patchwork-incoming@legolas.ozlabs.org","gcc-patches@gcc.gnu.org"],"Authentication-Results":["legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=I3LWNevr;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=gcc.gnu.org\n (client-ip=2620:52:6:3111::32; helo=vm01.sourceware.org;\n envelope-from=gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org;\n receiver=patchwork.ozlabs.org)","sourceware.org;\n\tdkim=pass (2048-bit key,\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=I3LWNevr","sourceware.org;\n dmarc=pass (p=none dis=none) header.from=gmail.com","sourceware.org; spf=pass smtp.mailfrom=gmail.com","server2.sourceware.org;\n arc=none smtp.remote-ip=209.85.160.179"],"Received":["from vm01.sourceware.org (vm01.sourceware.org\n [IPv6:2620:52:6:3111::32])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange x25519 server-signature ECDSA (secp384r1) server-digest SHA384)\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4ftF535L4Rz1yH2\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 23:34:15 +1000 (AEST)","from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id B7C374BA23F4\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 13:34:13 +0000 (GMT)","from mail-qt1-f179.google.com (mail-qt1-f179.google.com\n [209.85.160.179])\n by sourceware.org (Postfix) with ESMTPS id 07FE74BA23D9\n for <gcc-patches@gcc.gnu.org>; Sat, 11 Apr 2026 13:33:31 +0000 (GMT)","by mail-qt1-f179.google.com with SMTP id\n d75a77b69052e-50b69bf5638so33454821cf.1\n for <gcc-patches@gcc.gnu.org>; Sat, 11 Apr 2026 06:33:31 -0700 (PDT)","from alvaro-MS-7D37.verizon.net\n ([2600:4041:592d:7300:b8f7:c631:1ab:88b0])\n by smtp.gmail.com with ESMTPSA id\n 6a1803df08f44-8ac84caace4sm47629306d6.39.2026.04.11.06.33.30\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Sat, 11 Apr 2026 06:33:30 -0700 (PDT)"],"DKIM-Filter":["OpenDKIM Filter v2.11.0 sourceware.org B7C374BA23F4","OpenDKIM Filter v2.11.0 sourceware.org 07FE74BA23D9"],"DMARC-Filter":"OpenDMARC Filter v1.4.2 sourceware.org 07FE74BA23D9","ARC-Filter":"OpenARC Filter v1.0.0 sourceware.org 07FE74BA23D9","ARC-Seal":"i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1775914412; cv=none;\n b=AFAK4GOOsEsXiW0eEVZTzj4T8mp1h9h32+NUN453wgi8KOwSVTi1k/zVPQw2ICbzirYd4v3kZ3XAgpExVieozLmJoxqDaFW39uZhZtk9eM2Wke9AhnICiY4JPfMb7zI2gON/JcSO2W44XxR3SUjkvPXeWOuhE3RMCdZ3/+VJ89g=","ARC-Message-Signature":"i=1; a=rsa-sha256; d=sourceware.org; s=key;\n t=1775914412; c=relaxed/simple;\n bh=4uWL3O57VwJyZuYRntijRiIAGNVd6E4uIhR7/Mfnbe8=;\n h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version;\n b=QqN3+ZMm6FG2nB4MLNyobrJtlsn7rnzFHlUktjSK6ACRkzETF5TSm1LfXw1BM7BMAf1FN23uoT+E6J0318GZB9Z9mmoegHwb5H4c9ucFAAcHG6r+PZB+adnqLBp2KHKZ14aLPM4LJxsoR84moS5qHP9fpKBOCIJnoSIWuwsPrAk=","ARC-Authentication-Results":"i=1; server2.sourceware.org","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=gmail.com; s=20251104; t=1775914411; x=1776519211; darn=gcc.gnu.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=QO2Gr0oTK4kwEjo0pmcZPbMIgk0f8lZiDUFjaV+UUs4=;\n b=I3LWNevrAjmmwPwGK0++LOKQz3+ln581k2dnwyBoK/Uoh61brYDQfaGRrm0s96qxhE\n vlNFh+GGeV1RGhU2ileZivEChstSwVNaWrzS9S/+7+dW7LN/1NGDD+G3z0I3eOrSkUR4\n wKUPMQCrPb2g3DyKB6/lrnU/cOr3kzziK1GL2Ibs2D56ZT2X1FzSLKPOQoimOV1Wm6YN\n RDcx5T+m6wm4uXJREB3474oeU0/wzAGvxB8CA+uK81SdDnrgVLgptIKQM8w3hPUIdEpk\n efU9/2Po8L4zM/OqF7Ivs/QKWjJWAYHf1gb0qoBiGSs0tSGlrnYMIJ2R2NEXNpowQqUb\n nhfQ==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1775914411; x=1776519211;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n :to:cc:subject:date:message-id:reply-to;\n bh=QO2Gr0oTK4kwEjo0pmcZPbMIgk0f8lZiDUFjaV+UUs4=;\n b=VZDpzlfaWUKiQtfZ4oYyt0hJ4mha0o5XzRQ/OeJYhJPEWqsgGI4cLrhN/IZvVpRpAT\n HWXIj6BmCWOGXIQjwyYIgTD2h401ET7GZT4oPHpFBbWXmfsaiIPNdqq6vU3Ek2xMWaB7\n pHM4oDQ+NsIOzHuL12bvvird8ah4gPRkY/YZDV04SvLqOCRcfhV3LRsmUDqSTj349S3d\n ZGtWKEaHR9/GFkbNEPtzWoiX0iL3klCbn+Auy4L5HB6ZPbTPcLBUpVksreVQo2f1qSsm\n LQjiwQ5QjJ1OrnYsDWf3fBF/oPVAGBnxYtIz21nQxLmsKMGMbSc4//7l51zmlWOYXUII\n +K3Q==","X-Gm-Message-State":"AOJu0Yzu8kMtSxteLDyL+9UI5IviPIsVZ73Hi6teCPX/igH2UR/6Fbkv\n bxQR3sAjWcszCGbS/+GbwSKUBXZIzS1zUsEe5pC+aaI6smWQHOfti1ea/pLUZg==","X-Gm-Gg":"AeBDiesEMHUs9D7ryxi3I3XUX1Zv4B9LLDQdgkf4l6TanJweYKiw5GkPZZjQrPvZ/AH\n UrK4rIf2/IE0xlPo2DYo0LocPMAMkkRe9h3ocMGqr226zZ/m6lALVZmQ0uCmQ139JV2ekIxcxEk\n WcOWCCn+EK8F1uYOZ8qNbs/OwuG/uiR/3i/173WEd/aguItbTmWtBfX+x4jcwb0KSlyjQH4ROBU\n qn8R0jAFlLedDSRYhpx3m6ExIGcgJSQMSX6gJCtpHP9yB4aTxtYGCZSC00qAx1LfpIn5/0KG3Ti\n 2zcLx0x9JJNNeWhwwLDDNM0CWHvhsgbz8nVzMtnD+w4K1+5Y+w0PO1HmKbwB6fT0QWRetgzXYTz\n pD15HYNCgE6GgumcKBHhNbFGS9t8MP4Bdf+G2zWfUNOkpOg3qrzcBvh6F+zksJN9nkHLBakxMTR\n gAbx6TCSJwegvmKA3FVQPQEH8lKhi7WrUFv45umxJ44JSQLy9CLjYrPr1/ZAHSaqR5GIyEeK96J\n wxxzt2hr2OkN6uNW73rBxJ0Ck3rH5B7RW5cXWoI6bDjyv2b01JxlUK8D1aM4nnP03V3wfUN0XX3\n 71vq","X-Received":"by 2002:a05:622a:9009:b0:509:30b0:8323 with SMTP id\n d75a77b69052e-50dd5cc4ed4mr99451401cf.31.1775914411105;\n Sat, 11 Apr 2026 06:33:31 -0700 (PDT)","From":"Alvaro Begue <alvaro.begue@gmail.com>","To":"gcc-patches@gcc.gnu.org","Cc":"jwakely@redhat.com, libstdc++@gcc.gnu.org,\n Alvaro Begue <alvaro.begue@gmail.com>","Subject":"[PATCH 5/5] libstdc++: Implement zic writezone merge optimization [PR\n 124854]","Date":"Sat, 11 Apr 2026 09:33:09 -0400","Message-Id":"\n <c2e2e6d472dd8842a2fff5fa7c85634a5c19f08e.1775912642.git.alvaro.begue@gmail.com>","X-Mailer":"git-send-email 2.34.1","In-Reply-To":"<cover.1775912642.git.alvaro.begue@gmail.com>","References":"<cover.1775912642.git.alvaro.begue@gmail.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","X-BeenThere":"gcc-patches@gcc.gnu.org","X-Mailman-Version":"2.1.30","Precedence":"list","List-Id":"Gcc-patches mailing list <gcc-patches.gcc.gnu.org>","List-Unsubscribe":"<https://gcc.gnu.org/mailman/options/gcc-patches>,\n <mailto:gcc-patches-request@gcc.gnu.org?subject=unsubscribe>","List-Archive":"<https://gcc.gnu.org/pipermail/gcc-patches/>","List-Post":"<mailto:gcc-patches@gcc.gnu.org>","List-Help":"<mailto:gcc-patches-request@gcc.gnu.org?subject=help>","List-Subscribe":"<https://gcc.gnu.org/mailman/listinfo/gcc-patches>,\n <mailto:gcc-patches-request@gcc.gnu.org?subject=subscribe>","Errors-To":"gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org"},"content":"Two distinct correctness fixes that together let lazy expansion match\nzic.c's writezone output for zones with rule firings near zone-line\nboundaries.\n\n1. Always seed info.offset and info.save from the active rule.\n\n   Previously, _M_get_sys_info skipped the find_active_rule lookup\n   when `letters` had already been populated from i[-1].next_letters().\n   That happens during a re-entry of partial lazy expansion: a prior\n   call cut off after num_after sys_infos and left an expanded\n   ZoneInfo whose next_letters() field is the letters of the rule\n   that should fire at the next batch's start.\n\n   The skip is wrong because info.offset and info.save are still at\n   their (ri.offset(), 0) initialization values when entering the\n   loop.  Without re-running the seeding, the first sys_info of the\n   new batch is emitted with stdoff alone for offset and save=0,\n   even though the line had non-zero save in force at info.begin.\n\n   Canonical breakage: Europe/Berlin around 1947-06-29.  With\n   num_after=4, batch 1 stops mid-line; batch 2 re-enters with\n   non-empty letters and emits a 2-hour CEST sys_info that has\n   offset=3600 (CET's offset) and save=0, instead of offset=7200\n   save=60 — observably wrong total offset for two hours.\n\n   Fix: pull the seeding (find_active_rule + info.offset/save\n   assignment) out of the `if (letters.empty())` branch and run it\n   unconditionally.  Only the letters fallback (first_std lookup)\n   stays gated on letters being empty.\n\n2. Add zic.c writezone merge optimization for backward jumps at\n   zone-line boundaries.\n\n   When two adjacent Zone lines have different total offsets and the\n   new line's rule set has a rule firing within |jump| of the\n   boundary (where jump = new_total - old_total < 0, i.e. local time\n   goes backward at the boundary), zic folds that rule into the\n   boundary itself: the single transition emitted has the rule's\n   save value already applied, so the new line begins with the\n   post-rule save rather than briefly using the pre-rule save and\n   then transitioning again moments later.\n\n   Canonical examples handled by the new merge block:\n     * America/Argentina/Buenos_Aires 1999-10-03: lines change\n       stdoff -3 → -4 with an Argentina DST rule firing on the same\n       day.  Without the merge, chrono emits a 1-hour stretch of\n       offset=-4 save=0 and then transitions to offset=-3 save=1;\n       with the merge, the boundary itself is at offset=-3 save=1.\n     * Europe/Berlin 1945-05-24: lines split a rule set, with the\n       So 1945-May-24 rule (save=2, \"CEMT\") firing at 01:00 UTC in\n       the new frame, inside the 1h backward window.\n\n   The merge block runs only at the first sys_info of a zone line,\n   not on partial-expansion re-entry.  We detect this by checking\n   that i[-1].next_letters() is empty: a mid-line re-entry's prior\n   ZoneInfo always has non-empty next_letters() (the rule firing\n   at the new batch's start), whereas a zone-line transition's\n   prior ZoneInfo ends with empty next_letters() because its\n   line's last forward-walk iteration emits with letters cleared.\n\nlibstdc++-v3/ChangeLog:\n\n\tPR libstdc++/124854\n\t* src/c++20/tzdb.cc (time_zone::_Impl::_M_get_sys_info):\n\tAlways run find_active_rule to seed info.offset and info.save,\n\tregardless of whether letters was already populated from\n\ti[-1].next_letters().  Add a writezone merge optimization\n\tblock at zone-line boundaries with backward jumps, gated on\n\tan empty next_letters() to avoid running on partial-expansion\n\tre-entry.\n\t* testsuite/std/time/time_zone/zone_merge.cc: New test.\n---\n libstdc++-v3/src/c++20/tzdb.cc                | 154 ++++++++++++------\n .../std/time/time_zone/zone_merge.cc          | 101 ++++++++++++\n 2 files changed, 208 insertions(+), 47 deletions(-)\n create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc","diff":"diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc\nindex 76ec956b5..0da232834 100644\n--- a/libstdc++-v3/src/c++20/tzdb.cc\n+++ b/libstdc++-v3/src/c++20/tzdb.cc\n@@ -1052,54 +1052,117 @@ namespace std::chrono\n     if (i != infos.begin() && i[-1].expanded())\n       letters = i[-1].next_letters();\n \n-    if (letters.empty())\n-      {\n-\t// We want the rule whose effect is in force at info.begin --\n-\t// including a rule that fires at exactly info.begin (its effect\n-\t// has just begun and is active for the first sys_info we are\n-\t// about to generate).  The search below uses a strict\n-\t// `rule_start < t` comparison, so pass info.begin + 1s to make\n-\t// the half-open lookup (..., info.begin] inclusive of the\n-\t// boundary instant.  This is what makes named-rule zone lines\n-\t// like Africa/Algiers (PR 116110) seed with the correct save:\n-\t// the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's\n-\t// frame, which is exactly the new line's begin.\n-\tsys_seconds t = info.begin + seconds(1);\n-\n-\t// Try to find a Rule active before this time, to get initial\n-\t// SAVE and LETTERS values.  See find_active_rule for the search\n-\t// semantics.\n-\tconst Rule* active_rule = find_active_rule(rules, t, ri.offset());\n-\n-\tif (active_rule)\n-\t  {\n-\t    info.offset = ri.offset() + active_rule->save;\n-\t    info.save = chrono::duration_cast<minutes>(active_rule->save);\n+    // Seed info.offset and info.save from the rule whose effect is in\n+    // force at info.begin.  This must run even when `letters` was\n+    // already populated from i[-1].next_letters() (which happens\n+    // during a re-entry of partial lazy expansion: the previous batch\n+    // left an expanded ZoneInfo whose next_letters() field is the\n+    // letters for the first sys_info of this batch), because\n+    // info.offset/save are still at their stdoff/0 init values and\n+    // would otherwise carry through into the first emitted sys_info\n+    // with the wrong total offset.\n+    //\n+    // The search uses a strict `rule_start < t` comparison, so pass\n+    // info.begin + 1s to make the half-open lookup (..., info.begin]\n+    // inclusive of the boundary instant.  This is what makes named-\n+    // rule zone lines like Africa/Algiers (PR 116110) seed with the\n+    // correct save: the Oct-21 rule fires at Oct 20 23:00 UTC in the\n+    // new line's frame, which is exactly the new line's begin.\n+    {\n+      sys_seconds t = info.begin + seconds(1);\n+      const Rule* active_rule = find_active_rule(rules, t, ri.offset());\n+      if (active_rule)\n+\t{\n+\t  info.offset = ri.offset() + active_rule->save;\n+\t  info.save = chrono::duration_cast<minutes>(active_rule->save);\n+\t  if (letters.empty())\n \t    letters = active_rule->letters;\n-\t  }\n-\telse\n+\t}\n+      else if (letters.empty())\n+\t{\n+\t  // No rule applies before info.begin; fall back to the LETTERS\n+\t  // of the earliest STD rule, since the period before the first\n+\t  // DST transition is conventionally standard time.\n+\t  const Rule* first_std = nullptr;\n+\t  for (const auto& rule : rules)\n+\t    {\n+\t      if (rule.save != minutes(0))\n+\t\tcontinue;\n+\t      if (!first_std)\n+\t\tfirst_std = &rule;\n+\t      else if (rule.from < first_std->from)\n+\t\tfirst_std = &rule;\n+\t      else if (rule.from == first_std->from)\n+\t\t{\n+\t\t  if (rule.start_time(rule.from, {})\n+\t\t\t< first_std->start_time(first_std->from, {}))\n+\t\t    first_std = &rule;\n+\t\t}\n+\t    }\n+\t  if (first_std)\n+\t    letters = first_std->letters;\n+\t}\n+    }\n+\n+    // zic.c writezone merge optimization.  When the previous zone\n+    // line's end total offset differs from this line's seeded total\n+    // and the local time would jump backward at the boundary, look\n+    // for a rule in this line's set that fires within the resulting\n+    // gap window and would compensate the jump.  zic folds such a\n+    // rule into the boundary transition; we mirror that here by\n+    // pulling the rule's save back to info.begin.\n+    //\n+    // Only runs at the first sys_info of a zone line (not on re-entry\n+    // mid-line during partial lazy expansion), which is detected by\n+    // an empty next_letters() on the prior expanded ZoneInfo: a mid-\n+    // line re-entry's prior ZoneInfo always has a non-empty\n+    // next_letters() (the letters of the rule that fires at the new\n+    // batch's start), whereas a zone-line transition's prior ZoneInfo\n+    // ends with empty next_letters() because the line's last\n+    // iteration emits with letters cleared.\n+    //\n+    // Canonical examples:\n+    //   * America/Argentina/Buenos_Aires 1999-10-03: lines\n+    //       -3 A -03/-02 1999 O 3\n+    //       -4 A -04/-03 2000 Mar 3\n+    //     have new_total = -4, prev_total = -3 (jump = -1h).  The\n+    //     1999 Oct Argentina rule fires at Oct 3 04:00 UTC in the\n+    //     new -4 frame, which is exactly info.begin + 1h, inside\n+    //     the 1h window.\n+    //   * Europe/Berlin 1945-05-24: lines\n+    //       1 c CE%sT 1945 May 24 2\n+    //       1 So CE%sT 1946\n+    //     have new_total = 1, prev_total = 2 (jump = -1h).  The So\n+    //     1945-May-24 rule (save = 2h, \"CEMT\") fires at 01:00 UTC\n+    //     in the new frame, inside the 1h window.\n+    if (i != infos.begin() && i[-1].expanded()\n+\t  && i[-1].next_letters().empty())\n+      {\n+\tsys_info prev;\n+\ti[-1].to(prev);\n+\tconst seconds prev_total = prev.offset;\n+\tconst seconds new_total = info.offset;\n+\tconst seconds jump = new_total - prev_total;\n+\tif (jump < 0s)\n \t  {\n-\t    // No rule applies before info.begin; fall back to the LETTERS\n-\t    // of the earliest STD rule, since the period before the first\n-\t    // DST transition is conventionally standard time.\n-\t    const Rule* first_std = nullptr;\n-\t    for (const auto& rule : rules)\n+\t    const seconds window = -jump;\n+\t    // Look for a rule firing in (info.begin, info.begin+window].\n+\t    const Rule* merge_rule\n+\t      = find_active_rule(rules,\n+\t\t\t\t info.begin + window + seconds(1),\n+\t\t\t\t ri.offset());\n+\t    if (merge_rule\n+\t\t&& merge_rule->start_time(year_month_day{\n+\t\t\t\t\t  chrono::floor<days>(\n+\t\t\t\t\t    info.begin + window)\n+\t\t\t\t\t  }.year(),\n+\t\t\t\t\t  ri.offset()) > info.begin)\n \t      {\n-\t\tif (rule.save != minutes(0))\n-\t\t  continue;\n-\t\tif (!first_std)\n-\t\t  first_std = &rule;\n-\t\telse if (rule.from < first_std->from)\n-\t\t  first_std = &rule;\n-\t\telse if (rule.from == first_std->from)\n-\t\t  {\n-\t\t    if (rule.start_time(rule.from, {})\n-\t\t\t  < first_std->start_time(first_std->from, {}))\n-\t\t      first_std = &rule;\n-\t\t  }\n+\t\tinfo.offset = ri.offset() + merge_rule->save;\n+\t\tinfo.save\n+\t\t  = chrono::duration_cast<minutes>(merge_rule->save);\n+\t\tletters = merge_rule->letters;\n \t      }\n-\t    if (first_std)\n-\t      letters = first_std->letters;\n \t  }\n       }\n \n@@ -1148,9 +1211,6 @@ namespace std::chrono\n \n \t    if (t < rule_start && rule_start < info.end)\n \t      {\n-\t\tif (rule_start - t < days(1)) // XXX shouldn't be needed!\n-\t\t  continue;\n-\n \t\t// Found a closer transition than the previous info.end.\n \t\tinfo.end = rule_start;\n \t\tnext_rule = &rule;\ndiff --git a/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc b/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc\nnew file mode 100644\nindex 000000000..87f55b9d6\n--- /dev/null\n+++ b/libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc\n@@ -0,0 +1,101 @@\n+// { dg-do run { target c++20 } }\n+// { dg-require-effective-target tzdb }\n+// { dg-require-effective-target cxx11_abi }\n+// { dg-xfail-run-if \"no weak override on AIX\" { powerpc-ibm-aix* } }\n+\n+// Regression test: zic.c's writezone merges a zone-line transition with\n+// a rule firing that would otherwise create a brief observably-wrong\n+// stretch of local time.  When two adjacent Zone lines have different\n+// total offsets and the new line's rule set has a rule firing within\n+// |jump| of the boundary (where jump = new_total - old_total < 0, i.e.\n+// local time goes backward at the boundary), zic folds that rule into\n+// the boundary itself: the single transition emitted has the rule's\n+// save value already applied, so the new line begins with the post-rule\n+// save rather than briefly using the pre-rule save and then transitioning\n+// again moments later.\n+//\n+// Two canonical real-world cases:\n+//   * America/Argentina/Buenos_Aires 1999-10-03 (lines change stdoff\n+//     -3 → -4 with an Argentina DST rule firing on the same day).\n+//   * Europe/Berlin 1945-05-24 (lines split a rule set, with the So\n+//     1945-May-24 rule firing inside the boundary's window).\n+//\n+// Mirror the Buenos Aires shape with a synthetic zone.\n+\n+#include <chrono>\n+#include <fstream>\n+#include <testsuite_hooks.h>\n+\n+static bool override_used = false;\n+\n+namespace __gnu_cxx\n+{\n+  const char* zoneinfo_dir_override() {\n+    override_used = true;\n+    return \"./\";\n+  }\n+}\n+\n+int\n+main()\n+{\n+  using namespace std::chrono;\n+\n+  // Argentina-style: stdoff jumps from -3 to -4 at the same instant\n+  // a save=1 (\"S\") rule fires.  In the new (-4) frame, the rule's\n+  // wall at_time of 00:00 is at UT 04:00, which is 1 hour after the\n+  // boundary at UT 03:00.  Without the merge optimization the new\n+  // line would seed with save=0 (offset -4, abbrev -04) for that 1\n+  // hour and then transition to save=1 (offset -3, abbrev -03);\n+  // with the merge, the boundary itself is at offset=-3, save=1.\n+  std::ofstream(\"tzdata.zi\") << R\"(# version test_zone_merge\n+R T 1999 o - O 3 0 1 -\n+R T 2000 o - Mar 3 0 0 -\n+Z Test/BA -3 -  %z  1999 O 3\n+          -4 T  %z  2000 Mar 3\n+          -3 -  %z\n+)\";\n+\n+  const auto& db = reload_tzdb();\n+  VERIFY( override_used );\n+  VERIFY( db.version == \"test_zone_merge\" );\n+\n+  auto* tz = locate_zone(\"Test/BA\");\n+\n+  // The boundary is the wall UNTIL \"1999 O 3\" (default time 00:00)\n+  // interpreted in the prior (-3) frame, i.e. UT 03:00 1999-10-03.\n+  sys_seconds boundary{sys_days(1999y/October/3) + 3h};\n+\n+  auto before = tz->get_info(boundary - 1s);\n+  VERIFY( before.offset == -3h );\n+  VERIFY( before.save == 0min );\n+  VERIFY( before.abbrev == \"-03\" );\n+\n+  // At the boundary the merge optimization kicks in: the second zone\n+  // line's first sys_info should already have save=1 from the Oct 3\n+  // rule, total offset -3h, abbrev \"-03\".  Without the fix, chrono\n+  // would emit a 1-hour stretch of save=0 (\"-04\") here.\n+  auto at_boundary = tz->get_info(boundary);\n+  VERIFY( at_boundary.offset == -3h );\n+  VERIFY( at_boundary.save == 60min );\n+  VERIFY( at_boundary.abbrev == \"-03\" );\n+\n+  auto plus_30min = tz->get_info(boundary + 30min);\n+  VERIFY( plus_30min.offset == -3h );\n+  VERIFY( plus_30min.save == 60min );\n+  VERIFY( plus_30min.abbrev == \"-03\" );\n+\n+  // Sanity: well after the boundary, still in the merged sys_info\n+  // until the Mar 3 2000 transition.\n+  auto winter = tz->get_info(sys_days(2000y/January/15));\n+  VERIFY( winter.offset == -3h );\n+  VERIFY( winter.save == 60min );\n+  VERIFY( winter.abbrev == \"-03\" );\n+\n+  // After Mar 3 2000: line 2 ends, line 3 begins.  No DST rule fires\n+  // at this boundary, so total offset reverts to -3h with save=0.\n+  auto spring = tz->get_info(sys_days(2000y/April/15));\n+  VERIFY( spring.offset == -3h );\n+  VERIFY( spring.save == 0min );\n+  VERIFY( spring.abbrev == \"-03\" );\n+}\n","prefixes":["5/5"]}