Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/1.1/patches/2222416/?format=api
{ "id": 2222416, "url": "http://patchwork.ozlabs.org/api/1.1/patches/2222416/?format=api", "web_url": "http://patchwork.ozlabs.org/project/gcc/patch/c2664d97c22d6ff1d8bea5c516e5fd5d1d704c77.1775912642.git.alvaro.begue@gmail.com/", "project": { "id": 17, "url": "http://patchwork.ozlabs.org/api/1.1/projects/17/?format=api", "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 }, "msgid": "<c2664d97c22d6ff1d8bea5c516e5fd5d1d704c77.1775912642.git.alvaro.begue@gmail.com>", "date": "2026-04-11T13:33:07", "name": "[3/5] libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "f7204deba954f9d2531ee262226c0590df0d11f1", "submitter": { "id": 93119, "url": "http://patchwork.ozlabs.org/api/1.1/people/93119/?format=api", "name": "Alvaro Begue", "email": "alvaro.begue@gmail.com" }, "delegate": null, "mbox": "http://patchwork.ozlabs.org/project/gcc/patch/c2664d97c22d6ff1d8bea5c516e5fd5d1d704c77.1775912642.git.alvaro.begue@gmail.com/mbox/", "series": [ { "id": 499557, "url": "http://patchwork.ozlabs.org/api/1.1/series/499557/?format=api", "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/2222416/comments/", "check": "pending", "checks": "http://patchwork.ozlabs.org/api/patches/2222416/checks/", "tags": {}, "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=bTYXrHyT;\n\tdkim-atps=neutral", "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=gcc.gnu.org\n (client-ip=38.145.34.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=bTYXrHyT", "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.222.176" ], "Received": [ "from vm01.sourceware.org (vm01.sourceware.org [38.145.34.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 4ftF7W6Ljzz1yGb\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 23:36:22 +1000 (AEST)", "from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id 595F84BA23D9\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 13:36:20 +0000 (GMT)", "from mail-qk1-f176.google.com (mail-qk1-f176.google.com\n [209.85.222.176])\n by sourceware.org (Postfix) with ESMTPS id E414D4BA23E4\n for <gcc-patches@gcc.gnu.org>; Sat, 11 Apr 2026 13:33:30 +0000 (GMT)", "by mail-qk1-f176.google.com with SMTP id\n af79cd13be357-8d68f702851so485229685a.0\n for <gcc-patches@gcc.gnu.org>; Sat, 11 Apr 2026 06:33:30 -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.29\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Sat, 11 Apr 2026 06:33:29 -0700 (PDT)" ], "DKIM-Filter": [ "OpenDKIM Filter v2.11.0 sourceware.org 595F84BA23D9", "OpenDKIM Filter v2.11.0 sourceware.org E414D4BA23E4" ], "DMARC-Filter": "OpenDMARC Filter v1.4.2 sourceware.org E414D4BA23E4", "ARC-Filter": "OpenARC Filter v1.0.0 sourceware.org E414D4BA23E4", "ARC-Seal": "i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1775914411; cv=none;\n b=E8pzGGFX5B+xohVS0ncZVjryZUKMrpT3nC1ZraNxHnKnr3op4ur2/XMovmvjdVaWDkHBLIq1Yp5ODQurOWqY0NurjHtxObzJNSG5vwjj4l1R3UcZhcd2eo/OqWHTYTQe+D53hNA6qmaK200XJ7mHSH0JBBFxk/8htERwRf/OZso=", "ARC-Message-Signature": "i=1; a=rsa-sha256; d=sourceware.org; s=key;\n t=1775914411; c=relaxed/simple;\n bh=X73P52cad3nQE6RqDVwa9HlC8hM99nAovhLkWAEtKGo=;\n h=DKIM-Signature:From:To:Subject:Date:Message-Id:MIME-Version;\n b=V1IMSeROFHYAK3PpECwRhBkYxiKGREmHlLwOBrFOJoPcpZlv5C7pbhdM5t9M2PBTM9vloDLpnuMeC+ugDOYjLCRwrFnH+TLL6IhjfvXNS1OfSIbQfM/nS4EBtC6PLb5zLRxZi2u0EXb9GlIuYyzw5xYgp1pj8raOF+qXjUmoDuY=", "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=1775914410; x=1776519210; 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=J5/zRcAl/OPjb9SJSjmGjvhwCOM5cK9b75R5GbQ2RE8=;\n b=bTYXrHyT6VFfxfBqlfssZQSkWxyQt41TL7acdDWslxT8GSGniEawf/4SdVBzWz8ZEZ\n omRAU1h3yJ1VIH+XOxdGjzbToQe9/BusdU+tt5Er53rhNFF1QELVz5lPpBkCVQIRw4b5\n mpKGtOGxykgq4gKyC6pd31CIMKS3/DpP6GOuZk5y4Qy0cG1XDcZPPB0pXPoJyDbyYpzi\n mRw3FPpe4w+9zpwBoOxd/D/wVLinI2dlBpbwRtzNgLrutwGRCN0UExc0QRjXbTndrP+b\n NoVavwQxYNKCfMBR1OR2daJXnno17YWhHEe8gQmH7NVjxaAZ1pWbUPiYMKNypfU9pfLJ\n fIPA==", "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1775914410; x=1776519210;\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=J5/zRcAl/OPjb9SJSjmGjvhwCOM5cK9b75R5GbQ2RE8=;\n b=O7WFon/M6o2fhe8qOMNVDbYmlZeODEKwv1zi5juhHZ4SDuw4q+FplAt6YDpnSnCm0f\n 27es/TYwPD+GgPv6qrvodFeGrCVOwy6O05P4w1DkPUpmi6WYQmgKXOXIYPj2B4ckGmVB\n o8dkij8OLJnalS79ubc+T3KLsdqBQUxUzEL2JC5Aqjx75yO0VQvSxvcAOvnHJONOjX7i\n J2qKWVaJxsOsaBGkmTCP67xJX3vKxqXyqf1JUsjxs3hBqBMmzYJdM6zQFl8Gl8zIbcY6\n kyvkn0Gz6fDYl2Uzekl/zftXUoNfpAYkYDdbRQlUDJv9GU6CgloedBry91Mej0vIxiiT\n aJXA==", "X-Gm-Message-State": "AOJu0Yx0kFaF8UuifhFLB91gvUvJiFIXKAdu1lyOpvvwEUvH+AuCkgHt\n r0Vg+kCuhhq4UIc8MJL+483fxVdjHI9HHHaUxRF+2AYxhd+WRdHu+8vUPT5o+Q==", "X-Gm-Gg": "AeBDiesSueuNUBBwz5iPruayS49tTA5MpI3WsO95HGxSwDYuSFCOjUMTGJ0GZKuJs8n\n V3Fu13h2vfqUChnWloGZNEzHQotXXdz9s9gkE0pg5WGI2ZbmkSlM4o+Lrm189CASVGzfA9CSCzT\n 2djIF8pIZ5a4InRAkcdOJgBkRjcaRMTOGQ0eps+2EBr4VRXEnQ4ldYPedLJhbNljKZnH7OjJV1w\n O4a/mnkv6IgXJlYqIYLrIX7fd53/8tBLHhr72V3Werv+r6zX3rQQq0d12SnS3KZuX+7CG3H0ArR\n h/0P2z0Xa6d1dhXpn7nUO+1gy72fRPQqxk3JKFUAbJhPlw/1jpvdNBcMEnCKbHxE99QCZxgNMkF\n K/raq4mp7duqtt+qr042lZe7rzhYcLYxVrKz+MswSUsTHs5OXGRKzwc9caZs832Z92DtjCLF0uV\n xgHhknbGMIIyR+lWf2fGY4Fip0K8vbXrmAx8QyyoJjg/nEFm3KV5yTNPAknLcv4gzcKmynu17Ci\n TjQQtvTHC0ZjNT2PgKNlGcUxeuUl/b7CRphWYK4nCe623ikDBML+7u+Lyp1usJz8jv01hiocgXF\n qLhs", "X-Received": "by 2002:a05:620a:4146:b0:8d6:2958:ec16 with SMTP id\n af79cd13be357-8ddd0592074mr1015487885a.60.1775914409921;\n Sat, 11 Apr 2026 06:33:29 -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 3/5] libstdc++: Resolve named-rule UNTIL save adjustment\n [PR116110]", "Date": "Sat, 11 Apr 2026 09:33:07 -0400", "Message-Id": "\n <c2664d97c22d6ff1d8bea5c516e5fd5d1d704c77.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-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": "The previous patches for PR 116110 left one case unresolved: a Zone\nline with a wall-time UNTIL whose RULES field is a named rule set.\nThe save value used to convert the wall UNTIL to UTC depends on which\nrule of the set was active at the UNTIL instant, but at parse time\nthe rule records have not all been loaded, so the active-rule lookup\ncan't yet be performed.\n\nThe remaining FIXME in operator>>(istream&, ZoneInfo&) caused zones\nlike Africa/Algiers (around 1977-10-21) to place their zone-line\nboundary one save-period off from the canonical zic interpretation,\nproducing brief incorrect sys_info windows during DST transitions.\n\nThis commit defers the save adjustment to a fixup pass run after\nranges::stable_sort(node->rules) at the end of reload_tzdb.\n\nA new ZoneInfo::m_until_save_pending bit (stolen from m_pos:15, which\nbecomes m_pos:14 -- still 16384 max, far above any realistic offset\ninto m_buf) marks pending entries. The parser sets this bit when\nit sees a wall-time UNTIL on a named-rule line and skips the save\nsubtraction. The fixup pass walks every zone's ZoneInfos, looks up\nthe active rule, and applies the deferred adjustment.\n\nThe active-rule lookup uses a new helper find_pre_until_rule() that\nwalks all (rule, year) pairs in chronological order and maintains a\nrunning save value, so wall-time rules' TIME fields are interpreted\nrelative to the cascaded save state. The boundary it compares\nagainst shrinks as the running save grows, which gives zic.c's\ninterpretation: a rule firing at exactly the wall UNTIL belongs to\nthe next zone line, not the current one.\n\nThe lazy-expansion seeding code that finds the active rule at\ninfo.begin is also updated to use a half-open `rule_start < t`\nwindow with `t = info.begin + 1s`, so a rule firing at exactly\ninfo.begin (the new line's first instant) is correctly identified\nas in force. Without this, the new line would seed with the wrong\nsave and the first sys_info would have the wrong total offset and\nabbreviation.\n\nThe test_apia case in 116110.cc had a hardcoded `+11h` workaround\nfor the unfixed bug; with this fix in place the workaround is removed\nand the value becomes the canonical `+10h`.\n\nlibstdc++-v3/ChangeLog:\n\n\tPR libstdc++/116110\n\t* src/c++20/tzdb.cc (ZoneInfo): Add m_until_save_pending bit\n\t(stolen from m_pos:15) and accessors until_save_pending(),\n\tset_until_save_pending(), clear_until_save_pending(), and\n\tadjust_until().\n\t(find_pre_until_rule): New function. Chronological cascade\n\twalker with iterative-boundary semantics, used by the post-\n\tparse fixup pass.\n\t(operator>>(istream&, ZoneInfo&)): Set m_until_save_pending\n\twhen the wall UNTIL on a named-rule line cannot have its save\n\tsubtracted at parse time. Replaces the FIXME.\n\t(time_zone::_Impl::_M_get_sys_info): Change the seeding active-\n\trule lookup to use t = info.begin + 1s, so a rule firing at\n\texactly info.begin is included.\n\t(reload_tzdb): After sorting node->rules, run a fixup pass over\n\tevery ZoneInfo with until_save_pending set, calling\n\tfind_pre_until_rule and adjust_until.\n\t* testsuite/std/time/time_zone/116110.cc (test_apia): Remove\n\tthe +11h workaround for the unfixed named-rule UNTIL bug; the\n\tcanonical +10h boundary is now produced.\n\t* testsuite/std/time/time_zone/pr116110_named.cc: New test.\n---\n libstdc++-v3/src/c++20/tzdb.cc | 165 +++++++++++++++++-\n .../testsuite/std/time/time_zone/116110.cc | 7 +-\n .../std/time/time_zone/pr116110_named.cc | 106 +++++++++++\n 3 files changed, 273 insertions(+), 5 deletions(-)\n create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc", "diff": "diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc\nindex 8cbf3213d..2e910c99d 100644\n--- a/libstdc++-v3/src/c++20/tzdb.cc\n+++ b/libstdc++-v3/src/c++20/tzdb.cc\n@@ -520,6 +520,26 @@ namespace std::chrono\n sys_seconds\n until() const noexcept { return m_until; }\n \n+ // PR 116110: When the parser sees a wall-time UNTIL on a named-Rule\n+ // line, the SAVE component of the conversion can't be applied yet\n+ // because the active Rule depends on the cumulative state of the\n+ // (not-yet-loaded) rule set. The parser leaves m_until in the\n+ // \"STDOFF subtracted but SAVE not yet subtracted\" intermediate\n+ // state and sets this bit; reload_tzdb runs a fixup pass after all\n+ // Rule records are loaded to subtract the SAVE.\n+ bool\n+ until_save_pending() const noexcept { return m_until_save_pending; }\n+\n+ void\n+ set_until_save_pending() noexcept { m_until_save_pending = 1; }\n+\n+ void\n+ clear_until_save_pending() noexcept { m_until_save_pending = 0; }\n+\n+ // Subtract `s` from m_until. Used by the PR 116110 fixup pass.\n+ void\n+ adjust_until(seconds s) noexcept { m_until -= s; }\n+\n friend istream& operator>>(istream&, ZoneInfo&);\n \n bool\n@@ -575,8 +595,9 @@ namespace std::chrono\n }\n \n string m_buf; // rules() + ' ' + format() OR letters + ' ' + format()\n- uint_least16_t m_pos : 15 = 0; // offset of format() in m_buf\n+ uint_least16_t m_pos : 14 = 0; // offset of format() in m_buf\n uint_least16_t m_expanded : 1 = 0;\n+ uint_least16_t m_until_save_pending : 1 = 0; // PR 116110, see above\n duration<int_least16_t, ratio<60>> m_save{};\n sec32_t m_offset{};\n sys_seconds m_until{};\n@@ -683,6 +704,97 @@ namespace std::chrono\n }\n #endif\n };\n+\n+ // Find the Rule whose save value is in force at the wall-time UNTIL\n+ // of a Zone line, given that `wall_minus_stdoff` is the line's UNTIL\n+ // expressed in the \"save=0\" frame (i.e. the parsed wall UNTIL with\n+ // the line's STDOFF subtracted) and `stdoff` is the line's standard\n+ // offset.\n+ //\n+ // The function walks all (rule, year) pairs in chronological order,\n+ // maintaining a running save value. Wall-time rules have their TIME\n+ // field interpreted relative to the running save (since \"wall\" means\n+ // local civil time = stdoff + save), so a rule's effective UT firing\n+ // time depends on which prior rule was last in force. This matches\n+ // zic.c's outzone() logic and is required for zone lines whose\n+ // rule set has rules whose at_time depends on cascading saves.\n+ //\n+ // The comparison `fire < boundary` shrinks `boundary` as the running\n+ // save cascades up: a rule that fires AT the boundary (with the\n+ // cascaded save applied) is treated as belonging to the next zone\n+ // line, and its save is excluded from the running total.\n+ //\n+ // Canonical case: Africa/Algiers 1977-10-21. The \"Algeria 1977\n+ // Oct 21\" rule (save=0) fires at the same instant as the wall UNTIL\n+ // of line 6 (\"0 d WE%sT 1977 O 21\"). The pre-rule save (1h, from\n+ // the May 6 rule) is what determines the boundary's UT placement,\n+ // not the Oct 21 rule's save=0.\n+ //\n+ // The calendar window is extended by one year on each side, to\n+ // catch rules whose wall-time at_time falls in early January or\n+ // late December but whose UT firing crosses a year boundary due\n+ // to a large stdoff or save.\n+ template<typename _RuleRange>\n+ const Rule*\n+ find_pre_until_rule(const _RuleRange& rules,\n+\t\t\t sys_seconds wall_minus_stdoff, seconds stdoff)\n+ {\n+\tif (rules.empty())\n+\t return nullptr;\n+\n+\tconst year last_year\n+\t = year_month_day{chrono::floor<days>(wall_minus_stdoff)}.year()\n+\t + years(1);\n+\tyear first_year = year::max();\n+\tfor (const auto& r : rules)\n+\t if (r.from < first_year)\n+\t first_year = r.from;\n+\tif (first_year > last_year)\n+\t return nullptr;\n+\n+\tstruct Pending\n+\t{\n+\t const Rule* rule;\n+\t year y;\n+\t sys_seconds approx_when;\n+\t};\n+\tvector<Pending> pending;\n+\tpending.reserve(64);\n+\tfor (year y = first_year; y <= last_year; ++y)\n+\t for (const auto& r : rules)\n+\t {\n+\t if (y < r.from || y > r.to)\n+\t\tcontinue;\n+\t seconds approx_off{};\n+\t if (r.when.indicator == at_time::Wall\n+\t\t || r.when.indicator == at_time::Standard)\n+\t\tapprox_off = stdoff;\n+\t pending.push_back({&r, y, r.start_time(y, approx_off)});\n+\t }\n+\tstd::sort(pending.begin(), pending.end(),\n+\t\t [](const Pending& a, const Pending& b) {\n+\t\t return a.approx_when < b.approx_when;\n+\t\t });\n+\n+\tseconds running_save{};\n+\tsys_seconds boundary = wall_minus_stdoff;\n+\tconst Rule* last_fired = nullptr;\n+\tfor (const auto& p : pending)\n+\t {\n+\t seconds offset{};\n+\t if (p.rule->when.indicator == at_time::Wall)\n+\t offset = stdoff + running_save;\n+\t else if (p.rule->when.indicator == at_time::Standard)\n+\t offset = stdoff;\n+\t sys_seconds fire = p.rule->start_time(p.y, offset);\n+\t if (fire >= boundary)\n+\t continue;\n+\t last_fired = p.rule;\n+\t running_save = p.rule->save;\n+\t boundary = wall_minus_stdoff - running_save;\n+\t }\n+\treturn last_fired;\n+ }\n } // namespace\n #endif // TZDB_DISABLED\n \n@@ -871,7 +983,17 @@ namespace std::chrono\n \n if (letters.empty())\n {\n-\tsys_seconds t = info.begin - seconds(1);\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 \tconst year_month_day date(chrono::floor<days>(t));\n \n \t// Try to find a Rule active before this time, to get initial\n@@ -1629,6 +1751,37 @@ namespace std::chrono\n ranges::sort(node->db.links, {}, &time_zone_link::name);\n ranges::stable_sort(node->rules, {}, &Rule::name);\n \n+ // PR 116110 fixup pass. For every Zone line whose UNTIL was a wall-\n+ // time expression on a named-rule line, the parser deferred the SAVE\n+ // adjustment because the active rule wasn't yet identifiable. Now\n+ // that all Rule records are loaded and indexed, walk every pending\n+ // ZoneInfo, find the rule whose effect was in force just before the\n+ // wall UNTIL, and subtract that rule's save from m_until.\n+ //\n+ // \"Just before the wall UNTIL\" matches zic.c's interpretation: the\n+ // wall time of UNTIL is read in the frame in effect immediately\n+ // prior to the boundary, so the SAVE used is the value that the\n+ // most recent rule strictly before the UNTIL set. A rule firing at\n+ // exactly the UNTIL is not yet in force at the moment the wall\n+ // time is being interpreted -- its effect belongs to the next zone\n+ // line, not this one. find_pre_until_rule's iterative-boundary\n+ // walker implements this semantics.\n+ for (const auto& tz : node->db.zones)\n+ {\n+\tauto& infos = tz._M_impl->infos;\n+\tfor (auto& info : infos)\n+\t {\n+\t if (!info.until_save_pending())\n+\t continue;\n+\t auto rules = ranges::equal_range(node->rules, info.rules(),\n+\t\t\t\t\t ranges::less{}, &Rule::name);\n+\t if (const Rule* r\n+\t\t = find_pre_until_rule(rules, info.until(), info.offset()))\n+\t info.adjust_until(seconds(r->save));\n+\t info.clear_until_save_pending();\n+\t }\n+ }\n+\n return Node::_S_replace_head(std::move(head), std::move(node));\n #else\n __throw_disabled();\n@@ -2400,7 +2553,13 @@ namespace std::chrono\n \t\t{\n \t\t if (inf.m_expanded) // Not a named Rule, SAVE is known now.\n \t\t inf.m_until -= inf.m_save;\n-\t\t // else Named Rule, SAVE is unknown. FIXME: PR 116110\n+\t\t else\n+\t\t // Named Rule: SAVE depends on which rule of the set\n+\t\t // was active at this instant, which can only be\n+\t\t // determined once all Rule records are loaded. Mark\n+\t\t // the ZoneInfo so that the fixup pass in reload_tzdb\n+\t\t // applies the deferred adjustment. PR 116110.\n+\t\t inf.set_until_save_pending();\n \t\t}\n \t }\n \t}\ndiff --git a/libstdc++-v3/testsuite/std/time/time_zone/116110.cc b/libstdc++-v3/testsuite/std/time/time_zone/116110.cc\nindex 26b9ba33c..4dd6647e1 100644\n--- a/libstdc++-v3/testsuite/std/time/time_zone/116110.cc\n+++ b/libstdc++-v3/testsuite/std/time/time_zone/116110.cc\n@@ -65,8 +65,11 @@ test_apia()\n auto* tz = locate_zone(\"Pacific/Apia\");\n local_seconds t = local_days(2011y/December/29) + 24h;\n \n- // FIXME: this should be + 10h but we do not account for DST yet, so + 11h.\n- sys_seconds ut(t.time_since_epoch() + 11h );\n+ // The wall UNTIL \"2011 Dec 29 24\" is interpreted in the prior offset\n+ // (-11h + save 1h = -10h), so the boundary is at local_days +24h +10h\n+ // (the FIXME for the +11h compensation has been resolved by the\n+ // fix for the named-rule UNTIL case in PR 116110).\n+ sys_seconds ut(t.time_since_epoch() + 10h );\n sys_info info;\n info = tz->get_info(ut - 1s);\n VERIFY( info.offset == (-11h + info.save) );\ndiff --git a/libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc b/libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc\nnew file mode 100644\nindex 000000000..b088ae8e5\n--- /dev/null\n+++ b/libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc\n@@ -0,0 +1,106 @@\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 for PR 116110, named-rule case.\n+//\n+// A Zone line whose RULES references a named Rule and whose UNTIL is a\n+// wall-time expression cannot have its UNTIL converted to a true UTC\n+// instant at parse time, because the SAVE value at the UNTIL depends on\n+// which rule of the named set was last in force just before that wall\n+// time -- and that's not known until all Rule records have been loaded\n+// and indexed. The \"Partial fix for interpretation of non-UTC UNTIL\n+// times\" commit handled the simpler cases (UNTIL with `s` indicator,\n+// `u` indicator, or wall + non-named RULES) but explicitly left a\n+// FIXME for the named-rule case. This test exercises that case via\n+// the canonical Africa/Algiers boundary at 1977-10-21.\n+//\n+// In this synthetic data:\n+// Rule d 1977 May 6 0:00 wall save=1\n+// Rule d 1977 Oct 21 0:00 wall save=0\n+// Z A 0 d WE%sT 1977 O 21\n+// 1 d CE%sT\n+//\n+// The first Zone line has STDOFF=0 and uses rule set d. The May rule\n+// sets save=1 (WEST, total +1). zic.c interprets the wall UNTIL\n+// \"1977 O 21\" using the SAVE value in force just before the boundary\n+// (i.e. May's save=1, since the Oct-21 rule in this line's frame fires\n+// at exactly the boundary, after the wall time has been read). So:\n+// wall(0:00) - stdoff(0) - save_just_before(1) = Oct 20 23:00 UTC\n+// is the correct UTC instant of the line's end.\n+//\n+// The second Zone line has STDOFF=1 and uses the same rule set d. In\n+// its own frame, the Oct-21 rule fires at exactly its starting instant\n+// (Oct 20 23:00 UTC = wall(0:00) - stdoff(1)), setting save=0. So at\n+// Oct 20 23:00 UTC the new line begins with stdoff=1, save=0, abbrev\n+// \"CET\", total offset +1. Both sides of the boundary have total +1;\n+// only the (stdoff, save) split changes. This is the merge that\n+// zdump shows for real Africa/Algiers in October 1977.\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+ std::ofstream(\"tzdata.zi\") << R\"(# version test_pr116110_named\n+R d 1977 o - May 6 0 1 S\n+R d 1977 o - O 21 0 0 -\n+Z Test/Algiers 0 d WE%sT 1977 O 21\n+ 1 d CE%sT\n+)\";\n+\n+ const auto& db = reload_tzdb();\n+ VERIFY( override_used );\n+ VERIFY( db.version == \"test_pr116110_named\" );\n+\n+ auto* tz = locate_zone(\"Test/Algiers\");\n+\n+ // Just before the boundary: still in the first Zone line under\n+ // the May-6 rule (save=1, WEST, total +1).\n+ auto pre = tz->get_info(sys_days{1977y/October/20} + 22h);\n+ VERIFY( pre.offset == 1h );\n+ VERIFY( pre.save == 1h );\n+ VERIFY( pre.abbrev == \"WEST\" );\n+\n+ // The \"active rule just before the wall UNTIL\" is May-6 (save=1),\n+ // so the wall UNTIL \"1977 O 21\" gets adjusted by stdoff(0)+save(1).\n+ // Without the fix, master leaves the line's m_until 1 hour too late\n+ // and the query just before Oct 21 00:00 UTC is in the wrong frame.\n+ // With the fix, queries strictly before the boundary stay in the\n+ // first line (WEST) and queries at/after the boundary are in the\n+ // second line (CET).\n+ auto at = tz->get_info(sys_days{1977y/October/20} + 23h);\n+ VERIFY( at.offset == 1h ); // stdoff 1 + save 0 (CET, second line)\n+ VERIFY( at.save == 0min );\n+ VERIFY( at.abbrev == \"CET\" );\n+\n+ // A second query inside the second line, well clear of the boundary.\n+ auto after = tz->get_info(sys_days{1977y/October/21} + 12h);\n+ VERIFY( after.offset == 1h );\n+ VERIFY( after.save == 0min );\n+ VERIFY( after.abbrev == \"CET\" );\n+\n+ // And a regression check that the boundary really moved: a query at\n+ // 1977-10-20 23:30 UTC must be in the SECOND line. Without the\n+ // fix, master's m_until for the first line is 1977-10-21 00:00 UTC,\n+ // and this query lands in the WET stretch produced by the first\n+ // line's expansion of the Oct-21 rule.\n+ auto window = tz->get_info(sys_days{1977y/October/20} + 23h + 30min);\n+ VERIFY( window.offset == 1h );\n+ VERIFY( window.abbrev == \"CET\" );\n+}\n", "prefixes": [ "3/5" ] }