{"id":2228543,"url":"http://patchwork.ozlabs.org/api/1.2/covers/2228543/?format=json","web_url":"http://patchwork.ozlabs.org/project/gcc/cover/CAF8dVMXKp=CN6mH8CGrSnhLc8k3vTGmuu2K2ae4xyxDu6eekxg@mail.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":"<CAF8dVMXKp=CN6mH8CGrSnhLc8k3vTGmuu2K2ae4xyxDu6eekxg@mail.gmail.com>","list_archive_url":null,"date":"2026-04-26T23:42:54","name":"[v2,0/5] libstdc++: chrono tzdb correctness fixes","submitter":{"id":93119,"url":"http://patchwork.ozlabs.org/api/1.2/people/93119/?format=json","name":"Álvaro Begué","email":"alvaro.begue@gmail.com"},"mbox":"http://patchwork.ozlabs.org/project/gcc/cover/CAF8dVMXKp=CN6mH8CGrSnhLc8k3vTGmuu2K2ae4xyxDu6eekxg@mail.gmail.com/mbox/","series":[{"id":501555,"url":"http://patchwork.ozlabs.org/api/1.2/series/501555/?format=json","web_url":"http://patchwork.ozlabs.org/project/gcc/list/?series=501555","date":"2026-04-26T23:42:54","name":"libstdc++: chrono tzdb correctness fixes","version":2,"mbox":"http://patchwork.ozlabs.org/series/501555/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/covers/2228543/comments/","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=r3KpmuzO;\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=r3KpmuzO","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=pass smtp.remote-ip=209.85.217.43"],"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 4g3jwd1K8Bz1yJX\n\tfor <incoming@patchwork.ozlabs.org>; Mon, 27 Apr 2026 09:44:47 +1000 (AEST)","from vm01.sourceware.org (localhost [127.0.0.1])\n\tby sourceware.org (Postfix) with ESMTP id C0A494BABF16\n\tfor <incoming@patchwork.ozlabs.org>; Sun, 26 Apr 2026 23:44:45 +0000 (GMT)","from mail-vs1-f43.google.com (mail-vs1-f43.google.com\n [209.85.217.43])\n by sourceware.org (Postfix) with ESMTPS id D06EE4BABF11\n for <gcc-patches@gcc.gnu.org>; Sun, 26 Apr 2026 23:43:31 +0000 (GMT)","by mail-vs1-f43.google.com with SMTP id\n ada2fe7eead31-6221c7251e1so897154137.3\n for <gcc-patches@gcc.gnu.org>; Sun, 26 Apr 2026 16:43:31 -0700 (PDT)"],"DKIM-Filter":["OpenDKIM Filter v2.11.0 sourceware.org C0A494BABF16","OpenDKIM Filter v2.11.0 sourceware.org D06EE4BABF11"],"DMARC-Filter":"OpenDMARC Filter v1.4.2 sourceware.org D06EE4BABF11","ARC-Filter":"OpenARC Filter v1.0.0 sourceware.org D06EE4BABF11","ARC-Seal":["i=2; a=rsa-sha256; d=sourceware.org; s=key; t=1777247012; cv=pass;\n b=LJ3CAza8erP5bdmF98daaAmktOz5ObFRc0Nrq0k7v6HjVV6KwLXQ/nvV5OtMZ1bftyvNBCWmCDGHralGQ8a1I3gQoNRlC6KsVJydXphZdaae2oRs7RNlu+FNhnD/KO6WHZxVY4UKNfrMNLo2rCgKfIAxSxv6+Py7EfWkDtxlBoc=","i=1; a=rsa-sha256; t=1777247011; cv=none;\n d=google.com; s=arc-20240605;\n b=AYRzPyt54Q3D8tTmW1N3omRVYYWTvqGdYZywt7FB2w8pfQMcN5IuM5VVNk6EWso5at\n 8ov6BP2nwRRJK9TMlGIlBrcMmcIO7hnbK1YAczMS/vrSMLBgDihGY2QurNg2bcMWUhWD\n LGA9rjV0+O/1F3gtHoOd6LtvroHugHQW/z442Rixa12vsmg/mETtPwiF10pf2bt0aD3v\n t0B4LZbCM1DqXXv3WmEfUAMBlhOCNYseq14h5L1TMP+pKFKGsuYMxpj0y0f7tYFV4SSV\n s8mH4TArAgJDUKinR6vbCNwBtPtvkqwasXrmc+PmzaOxcI9wVC03HXFKdNqFQqWE5IiR\n YQfw=="],"ARC-Message-Signature":["i=2; a=rsa-sha256; d=sourceware.org; s=key;\n t=1777247012; c=relaxed/simple;\n bh=/HCOf+odwyeGW8unPp2IHp+TVBeuZVVIlFOjPCf4eJ0=;\n h=DKIM-Signature:MIME-Version:From:Date:Message-ID:Subject:To;\n b=GBx8LS4/6/XZYC4RteJCasZecrn/UYMyQTH7i1Nzv7G75VwWEGrB5hKvxT5HBL497i90y1uyiw3+O0Flb3FWY5po6oihpEsTGWC46xzjX+6wKhBjl0ki3uix6FSWxoiXreXtr0UyAVbBTqos9cl5VcpNpK2jjcaFdCYV7mIPgu8=","i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;\n s=arc-20240605;\n h=cc:to:subject:message-id:date:from:mime-version:dkim-signature;\n bh=LFqYLKxlN4GbXuMcwyCpqWTav17zCOl7Nfn1kVct+e0=;\n fh=sWbBxiX87E8F1zPxXVV3X4tQqxV9sZLoQvmjvveliwE=;\n b=RXdGVBjLMYS7mJIBpgNtkAJSz+/krwrodFXLr/QRLAF3DMUDYOJdvRRcr7qRunuFM2\n 3v/qSFey0DZa8LljS1sqi5jS2lK1IaXZrjrjVmr+UJNpm0d5OMv+MSyv+mWzBY2V/8Zd\n JmQSLziOvHmJ9k9rN/Wn92Lygq96bPwi/oBY56wUvmLNCFr1PB5y2IGWVYFub7uiK2o/\n Chq9n+LCyNCjWCzORcp44TJ9WZFuZOptUxnFFM0vef+41C7CUsHWU7MIftQUWAil6I07\n L/533bkhs7s+gaRd+3qRMSZjYmL1NDEbyWlp6Az5KxGSJEEFFbHglObU+dRBTmLiGXOs\n Pc/g==; darn=gcc.gnu.org"],"ARC-Authentication-Results":["i=2; server2.sourceware.org","i=1; mx.google.com; arc=none"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=gmail.com; s=20251104; t=1777247011; x=1777851811; darn=gcc.gnu.org;\n h=cc:to:subject:message-id:date:from:mime-version:from:to:cc:subject\n :date:message-id:reply-to;\n bh=LFqYLKxlN4GbXuMcwyCpqWTav17zCOl7Nfn1kVct+e0=;\n b=r3KpmuzOXdCKEYyRBZ3S+pgdBm/r6SRjAuITUUGIuSKmgwddI3XMofFHlOxOtdRiTU\n D39i2n4QRSas+YzpzHpp60mOd5OJEIdc2yvCUpnusNdAicz3IXq98erevtRJR3f+h/uS\n bOMZUmFlr2RpKRz6q6V6H2qTPzjFrzzU2bPYe4jobFvsK19AlwCOWROvnVTH+9VT4Pc7\n 91nPjuc/xqx7I/15zpq2+sEDCU7w1YzwF+dgMe4BbqsJ8n4fJovXtr1gzLk8FSVevXnX\n gG5kEuHzcctJNg8p+2AXh2Va9Tg18PfqDg8IsIjCsNbfvZ3Blpo16FFNfV/WN1Sk3sBe\n 8/5A==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1777247011; x=1777851811;\n h=cc:to:subject:message-id:date:from:mime-version:x-gm-gg\n :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to;\n bh=LFqYLKxlN4GbXuMcwyCpqWTav17zCOl7Nfn1kVct+e0=;\n b=eCl6Pcw4+ZnNMpDRA8kNv91ISBf+LLry2lhQt73h3Ej1qpkLpCoRgd3o92MIaxVc6I\n C9QMvtYRptfU/RS9ucIZEdgKJW97BLoOB1122lwnDpZUxnzFAz9DrcWQaVyx2NyU/Uke\n rW3hq0VJ5+Wh8M0+8ZNdcv7XqHdS1cCyI58SLXkLJQAjp6cEuPvlTAnVJubK64LZ7BL6\n 1eweRn17mHntQ73Obe94hyGyaXnTWqPVBcyeLyeAr7pdv8nlVJtDAthg1KdEOQd6Xiwh\n 3ofi+luxCZguTHvVnfuKMtOZrGINSQ2FcESTAzCABFww34OdKNZDUFLNrCbZVI9OOSWp\n rMeg==","X-Gm-Message-State":"AOJu0YwR/1Rh6RmDWcbNMwcTkA1BBmPIUPVIHpcVVcNBuEGIAkqf3Pz3\n DabbMnNwCjHEwMW67m8BPNTjIzkBjoC06VoAb9+F5dtIzW05j022ShwxMzYlCQjxPLirPl/fL/d\n u5yiwl9rBR6EkIg5xke3PScHw40KnavQJ1rlxKCY=","X-Gm-Gg":"AeBDieuUIcP4jAjc1pxaWsGpZ7nTxfpbDB0Yoft/cv9fBYC9F3TtC0LgooXpKWAMerg\n 4cqZy2WsaSq7zbTgUaalFXKSVF6fJeSC//qbbFN+5RKp7F5/m5uG1EYtvj8tyktw8DVTcVB0iXd\n Ztzd6cdgZCimynFLMX1kgrBSowIsSPASYruj/guwpKweVh6TilsKSxu5gOa7panl1YwYfTAuB5I\n LZNWXxuxL7Tw5kK/cslrDhnMDY+b9aqQuXxXrpo4Co+6+gjBmWNQuXHCbh6elqaNJ5lnxWkI9Ex\n ts3qwvcnHxNEe4pSgdCL6NZxWpPK6e+vK/g0MyvvS04eNDfAgQZO80C2yVL4qxr8kmSKma6V4Cc\n 2MObko3avJ279i1E0sw9+uXNRPZVBfzAiLOSKXDCQ+1MRd4oTzl1VpnaQauuoip0nc2rKzw==","X-Received":"by 2002:a05:6102:512c:b0:608:6c7b:4554 with SMTP id\n ada2fe7eead31-616f68d370dmr18281045137.16.1777247010219; Sun, 26 Apr 2026\n 16:43:30 -0700 (PDT)","MIME-Version":"1.0","From":"=?utf-8?b?w4FsdmFybyBCZWd1w6k=?= <alvaro.begue@gmail.com>","Date":"Sun, 26 Apr 2026 19:42:54 -0400","X-Gm-Features":"AVHnY4JjzQX4EvMS7Zrm7VVB46BbCKnMGBzDKeOXwlG9TvXEkS3utIX4eCnxNuk","Message-ID":"\n <CAF8dVMXKp=CN6mH8CGrSnhLc8k3vTGmuu2K2ae4xyxDu6eekxg@mail.gmail.com>","Subject":"[PATCH v2 0/5] libstdc++: chrono tzdb correctness fixes","To":"gcc-patches@gcc.gnu.org","Cc":"jwakely@redhat.com, libstdc++@gcc.gnu.org, tkaminsk@redhat.com,\n\t=?utf-8?b?w4FsdmFybyBCZWd1w6k=?= <alvaro.begue@gmail.com>","Content-Type":"multipart/alternative; boundary=\"000000000000b8ebef0650659093\"","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":"This is v2 of the chrono tzdb correctness fix series.  The actual fixes\nare unchanged; v2 addresses Tomasz Kamiński's review comments, which\nwere all stylistic.\n\nChanges since v1 (per Tomasz's feedback):\n\n  * Trim verbose \"Regression test:\" / past-bug-history prose from the\n    test files.  Test comments now describe the behavior under test\n    rather than the historical bug.\n  * Trim redundant cross-reference comments and shorten the surviving\n    ones.  \"// PR 116110\" breadcrumbs in code comments are gone -- the\n    PR number is captured in the commit message and the ChangeLog entry.\n  * Patch 2: rename parse_on_day_body -> parse_day_spec; use C++20\n    designated initializers and if-with-initializer for `on_day` and\n    `abbrev_month`; restructure the optional DAY/TIME parse so TIME is\n    only attempted when DAY parsing succeeds.\n\nEach commit now carries a Signed-off-by trailer.\n\nA range-diff of v1 vs v2 is included at the end of this cover letter.\n\nTest plan (rerun against current master HEAD):\n\n  * libstdc++ stage1 build of GCC 16.0.1 trunk (b99e67e8a) succeeds.\n  * All 7 chrono tests pass (Wakely's existing 116110 + 124513 plus the\n    5 new ones added by this series).\n  * Brute-force comparison harness (628310 samples × 447 zones) reports\n    zero mismatches against libc localtime_r.\n  * Abbreviation-only sweep (188493 samples) reports zero offset and\n    zero abbrev diffs.\n\nThe series builds on Jonathan Wakely's recent PR116110 / PR124513\nwork (commits 663e5ade1, cddf4111c, fbc5d2b1a).  Patch 3 in particular\nresolves the \"FIXME: PR116110\" left in operator>>(istream&, ZoneInfo&)\nfor the named-rule wall-UNTIL case.\n\n  1. Fix numeric save offset on Zone lines [PR124851].\n\n     ZoneInfo::m_offset had inconsistent semantics: the parser path\n     stored stdoff alone, but the two sys_info-taking constructors\n     stored the total (stdoff + save).  Normalize m_offset to stdoff\n     alone everywhere; to() adds save back when reconstructing.\n\n  2. Support ON-format DAY in Zone UNTIL field [PR124852].\n\n     The UNTIL parser only accepted a plain integer as the DAY,\n     silently misparsing tzdata.zi entries like Europe/Simferopol's\n     \"1997 Mar lastSu 1u\".  Reuse the on_day machinery and the\n     parse_day_spec helper.\n\n  3. Resolve named-rule UNTIL save adjustment [PR116110].\n\n     The remaining FIXME in operator>>(istream&, ZoneInfo&) for\n     wall-time UNTILs on named-rule zone lines.  At parse time the\n     active rule cannot be evaluated, so the parser leaves the SAVE\n     adjustment pending and a fixup pass in reload_tzdb walks every\n     pending ZoneInfo and applies the adjustment using a new\n     find_pre_until_rule helper with iterative-boundary cascade\n     semantics.  Removes the +11h workaround from test_apia in\n     116110.cc.\n\n  4. Cascade wall-time saves in lazy expansion seeding [PR124853].\n\n     Replace the per-rule isolated active-rule lookup in\n     _M_get_sys_info with a chronological cascade walker that\n     maintains a running save and interprets each Wall-time rule's\n     at_time relative to the cascaded state (matching zic.c's\n     outzone()).\n\n  5. Implement zic writezone merge optimization [PR124854].\n\n     Two related fixes: (a) always seed info.offset/save from\n     find_active_rule (not just when letters is empty), so partial-\n     expansion re-entry sees the right state; (b) add the writezone\n     merge for backward jumps at zone-line boundaries.\n\nÁlvaro Begué (5):\n  libstdc++: Fix numeric save offset on Zone lines [PR124851]\n  libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]\n  libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]\n  libstdc++: Cascade wall-time saves in lazy expansion seeding\n    [PR124853]\n  libstdc++: Implement zic writezone merge optimization [PR124854]\n\n libstdc++-v3/src/c++20/tzdb.cc                | 378 ++++++++++++++----\n .../testsuite/std/time/time_zone/116110.cc    |   5 +-\n .../std/time/time_zone/numeric_save.cc        |  58 +++\n .../std/time/time_zone/pr116110_named.cc      |  74 ++++\n .../std/time/time_zone/until_day_on.cc        | 168 ++++++++\n .../std/time/time_zone/wall_cascade.cc        |  70 ++++\n .../std/time/time_zone/zone_merge.cc          |  84 ++++\n 7 files changed, 748 insertions(+), 89 deletions(-)\n create mode 100644\nlibstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc\n create mode 100644\nlibstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc\n create mode 100644\nlibstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc\n create mode 100644\nlibstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc\n create mode 100644 libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc\n\nRange-diff against v1:\n1:  7e77ac729 ! 1:  2136a9e20 libstdc++: Fix numeric save offset on Zone\nlines [PR 124851]\n    @@\n      ## Metadata ##\n    -Author: Alvaro Begue <alvaro.begue@gmail.com>\n    +Author: Álvaro Begué <alvaro.begue@gmail.com>\n\n      ## Commit message ##\n    -    libstdc++: Fix numeric save offset on Zone lines [PR 124851]\n    +    libstdc++: Fix numeric save offset on Zone lines [PR124851]\n\n         When a Zone line specifies a numeric value as its RULES field (the\n         constant DST save value for that zone line, e.g. Africa/Gaborone's\n    @@ Commit message\n                 populating sys_info::offset.\n                 * testsuite/std/time/time_zone/numeric_save.cc: New test.\n\n    +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>\n    +\n      ## libstdc++-v3/src/c++20/tzdb.cc ##\n     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n            }\n\n     -      // STDOFF: Seconds from UTC during standard time.\n    -+      // STDOFF: Seconds from UTC during standard time.  Always the\n    -+      // standard offset only; the saved value (if any) is in m_save\nand\n    -+      // is added back when reconstructing a sys_info via to().\n    ++      // STDOFF: Seconds from UTC during standard time (without any\nsave).\n            seconds\n            offset() const noexcept { return m_offset; }\n\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n\n      info.end = until();\n     - info.offset = offset();\n    -+ // m_offset is the standard offset only; add the saved value to\n    -+ // reconstruct the total offset.  See ZoneInfo's m_offset comment.\n     + info.offset = offset() + seconds(m_save);\n      info.save = minutes(m_save);\n      info.abbrev = format();\n    @@ libstdc++-v3/testsuite/std/time/time_zone/numeric_save.cc (new)\n     +// { dg-require-effective-target cxx11_abi }\n     +// { dg-xfail-run-if \"no weak override on AIX\" { powerpc-ibm-aix* } }\n     +\n    -+// Regression test: when a Zone line specifies a numeric value as its\n    -+// RULES field, that value is the constant DST save value for that\nzone\n    -+// line.  ZoneInfo::to() previously set sys_info::offset to the zone\n    -+// line's STDOFF only, ignoring the parsed save.  Per\n[time.zone.info.sys]\n    -+// sys_info::offset is the *total* UTC offset (stdoff + save), so any\n    -+// zone line with a non-zero numeric save reported the wrong offset.\n    -+//\n    -+// Mirrors Africa/Gaborone's tzdata, which uses\n    -+//   2 - CAT 1943 S 19 2\n    -+//   2 1 CAST 1944 Mar 19 2     <-- numeric \"1\" RULES, save = +1h\n    -+//   2 - CAT\n    -+// The middle line is what triggers the bug.\n    ++// When a Zone line specifies a numeric value as its RULES field, that\n    ++// value is the constant DST save value for that zone line.  Per\n    ++// [time.zone.info.sys] sys_info::offset is the total UTC offset\n    ++// (stdoff + save).\n     +\n     +#include <chrono>\n     +#include <fstream>\n2:  57b11cf6e ! 2:  60c6f5eef libstdc++: Support ON-format DAY in Zone\nUNTIL field [PR 124852]\n    @@\n      ## Metadata ##\n    -Author: Alvaro Begue <alvaro.begue@gmail.com>\n    +Author: Álvaro Begué <alvaro.begue@gmail.com>\n\n      ## Commit message ##\n    -    libstdc++: Support ON-format DAY in Zone UNTIL field [PR 124852]\n    +    libstdc++: Support ON-format DAY in Zone UNTIL field [PR124852]\n\n         The Zone-line UNTIL parser only accepted a plain day-of-month\ninteger\n         for the DAY field, while the tzdata.zi grammar accepts the same\nON-style\n    @@ Commit message\n         left d == 1 when the day token wasn't a digit, then went on to\nparse the\n         remainder as the TIME field.\n\n    -    Fix by reusing the existing parse_on_day_body() helper that already\n    +    Factor out the day-component parser from operator>>(istream&,\non_day&)\n    +    as parse_day_spec(), and reuse it for the UNTIL DAY field.\n parse_day_spec\n         handles all three on_day forms (DayOfMonth, LastWeekday, LessEq /\n    -    GreaterEq) for Rule lines. The MONTH-only and YEAR-only short\nforms are\n    -    still accepted because the DAY/TIME fields are optional and\ndefault to\n    -    day 1, time 00:00. The on_day struct's pin() method handles the\n    -    year/month-relative resolution.\n    +    GreaterEq).  The MONTH-only and YEAR-only short forms are still\naccepted\n    +    because the DAY/TIME fields are optional and default to day 1,\ntime 00:00.\n    +    The on_day struct's pin() method handles the year/month-relative\n    +    resolution.\n\n         The DAY field is unambiguously distinguishable from a TIME field\nthat\n         could otherwise follow the MONTH directly: per zic's grammar, MONTH\n    -    must be followed by DAY before any TIME is allowed. So we always\n    +    must be followed by DAY before any TIME is allowed.  So we always\n         attempt to parse a DAY if any non-whitespace remains after the\nMONTH.\n\n         libstdc++-v3/ChangeLog:\n\n                 PR libstdc++/124852\n    -            * src/c++20/tzdb.cc (parse_on_day_body): Factor out the\nday-\n    -            component parser from operator>>(istream&, on_day&) so it\ncan\n    -            be reused.\n    -            (operator>>(istream&, on_day&)): Use the new helper.\n    +            * src/c++20/tzdb.cc (parse_day_spec): New function,\nfactored\n    +            out of operator>>(istream&, on_day&).\n    +            (operator>>(istream&, on_day&)): Use parse_day_spec.\n                 (operator>>(istream&, ZoneInfo&)): Replace the integer DAY\n    -            parser with parse_on_day_body for the UNTIL field.\n    +            parser with parse_day_spec for the UNTIL field.\n                 * testsuite/std/time/time_zone/until_day_on.cc: New test.\n\n    +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>\n    +\n      ## libstdc++-v3/src/c++20/tzdb.cc ##\n     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n            }\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     +    // Read the day-component of an on_day expression (everything\nafter the\n     +    // month).  Three forms are accepted: a plain day-of-month number,\n     +    // \"lastXxx\" where Xxx is a weekday name (LastWeekday), or\n\"Xxx<=N\" or\n    -+    // \"Xxx>=N\" (LessEq / GreaterEq).  The caller is responsible for\nsetting\n    -+    // `on.month` before calling.  On failure the function sets\nfailbit and\n    -+    // leaves `on` unchanged.\n    ++    // \"Xxx>=N\" (LessEq / GreaterEq).  On failure the function sets\nfailbit\n    ++    // and leaves `on` unchanged.\n     +    istream&\n    -+    parse_on_day_body(istream& in, on_day& on)\n    ++    parse_day_spec(istream& in, on_day& on)\n          {\n     -      on_day on{};\n     -      abbrev_month m{};\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     +      abbrev_month m{};\n     +      in >> m;\n     +      on.month = static_cast<unsigned>(m.m);\n    -+      if (parse_on_day_body(in, on))\n    ++      if (parse_day_spec(in, on))\n     + to = on;\n     +      return in;\n     +    }\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n          {\n            int sign = 1;\n     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n    +       in.exceptions(ios::goodbit); // Don't throw ios::failure if\nYEAR absent.\n            if (int y = int(year::max()); in >> y)\n      {\n    -  abbrev_month m{January};\n    +-  abbrev_month m{January};\n     -  int d = 1;\n    -+  on_day on{};\n    -+  on.kind = on_day::DayOfMonth;\n    -+  on.month = 1;          // default January\n    -+  on.day_of_month = 1;   // default day-of-month 1\n    ++  on_day on{.kind = on_day::DayOfMonth, .month = 1, .day_of_month =\n1};\n       at_time t{};\n     -  // XXX DAY should support ON format, e.g. lastSun or Sun>=8\n     -  in >> m >> d >> t;\n     -  inf.m_until = sys_days(year(y)/m.m/day(d)) + seconds(t.time);\n    -+  if (in >> m)\n    ++  if (abbrev_month m{January}; in >> m)\n     +    {\n     +      on.month = static_cast<unsigned>(m.m);\n    -+      // The DAY field is optional.  Per the tzdata.zi grammar,\n    -+      // a MONTH followed by anything more is always followed by\n    -+      // a DAY (possibly followed by a TIME); MONTH directly\n    -+      // followed by TIME is not a valid form.  So if there's\n    -+      // any non-whitespace before end of line, parse a DAY.\n     +      if (!ws(in).eof())\n    -+ parse_on_day_body(in, on);\n    ++ if (parse_day_spec(in, on))\n    ++  in >> t;\n     +    }\n    -+  in >> t;\n     +  year_month_day ymd = on.pin(year(y));\n     +  inf.m_until = sys_days(ymd) + seconds(t.time);\n       if (t.indicator != at_time::Universal)\n    @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)\n     +// { dg-require-effective-target cxx11_abi }\n     +// { dg-xfail-run-if \"no weak override on AIX\" { powerpc-ibm-aix* } }\n     +\n    -+// Regression test: the DAY portion of a Zone line's UNTIL field\naccepts\n    -+// not only a numeric day-of-month but also \"lastXxx\" (last weekday in\n    -+// the month) and \"Xxx<=N\" / \"Xxx>=N\" forms, just like the ON field of\n    -+// a Rule line.  Previously the UNTIL parser used `int d; in >> d;`\nwhich\n    -+// silently failed on the non-numeric forms and defaulted d to 1,\nplacing\n    -+// any zone-line transition with such an UNTIL on the wrong calendar\nday.\n    ++// The DAY portion of a Zone line's UNTIL field accepts not only a\n    ++// numeric day-of-month but also \"lastXxx\" (last weekday in the month)\n    ++// and \"Xxx<=N\" / \"Xxx>=N\" forms, just like the ON field of a Rule\nline.\n     +//\n     +// Real-world example: Europe/Simferopol has\n     +//   3 - MSK 1997 Mar lastSu 1u\n    -+// which must place the boundary on 1997-03-30 (the last Sunday of\nMarch),\n    -+// not on 1997-03-01.\n    ++// which places the boundary on 1997-03-30 (the last Sunday of March).\n     +\n     +#include <chrono>\n     +#include <fstream>\n    @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)\n     +  auto at = tz->get_info(boundary);\n     +  VERIFY( at.abbrev == \"X\" );\n     +\n    -+  // Critical regression check: a sample 15 days BEFORE the boundary\nmust\n    -+  // still be in the MSK line.  The unfixed parser placed the\nboundary on\n    -+  // March 1 because \"lastSu\" defaulted to day 1, and a March-15 query\n    -+  // landed in the X line instead.\n    ++  // Check that the lastSu day is parsed correctly, and not defaulted\n    ++  // to the 1st: a March 15 query must still be in the MSK line.\n     +  auto mid_march = tz->get_info(sys_days{1997y/March/15});\n     +  VERIFY( mid_march.abbrev == \"MSK\" );\n     +  VERIFY( mid_march.offset == 3h );\n    @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)\n     +  auto at = tz->get_info(boundary);\n     +  VERIFY( at.abbrev == \"B\" );\n     +\n    -+  // A June-1 query must still be in the A line (the unfixed parser\n    -+  // placed the boundary on June 1).\n    ++  // Check that Sun>=8 is parsed correctly, and not defaulted to the\n1st.\n     +  auto early = tz->get_info(sys_days{1990y/June/1});\n     +  VERIFY( early.abbrev == \"A\" );\n     +}\n    @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)\n     +{\n     +  using namespace std::chrono;\n     +\n    -+  // Sanity check: a UNTIL with only a year (no MONTH, no DAY, no\nTIME)\n    -+  // must continue to default to January 1 00:00.\n    ++  // MONTH, DAY and TIME default to January 1st 00:00 if not\nspecified.\n     +  std::ofstream(\"tzdata.zi\") << R\"(# version test_year_only\n     +Z Test/YearOnly 0 - A 1990\n     +                0 - B\n    @@ libstdc++-v3/testsuite/std/time/time_zone/until_day_on.cc (new)\n     +{\n     +  using namespace std::chrono;\n     +\n    -+  // Sanity check: UNTIL with only YEAR and MONTH (no DAY, no TIME)\n    -+  // must default DAY to 1 and TIME to 00:00.\n    ++  // DAY and TIME default to the 1st 00:00 if not specified.\n     +  std::ofstream(\"tzdata.zi\") << R\"(# version test_year_month_only\n     +Z Test/YearMonth 0 - A 1990 Jul\n     +                 0 - B\n3:  38e5ea518 ! 3:  5b20eac08 libstdc++: Resolve named-rule UNTIL save\nadjustment [PR116110]\n    @@\n      ## Metadata ##\n    -Author: Alvaro Begue <alvaro.begue@gmail.com>\n    +Author: Álvaro Begué <alvaro.begue@gmail.com>\n\n      ## Commit message ##\n         libstdc++: Resolve named-rule UNTIL save adjustment [PR116110]\n    @@ Commit message\n                 (operator>>(istream&, ZoneInfo&)): Set m_until_save_pending\n                 when the wall UNTIL on a named-rule line cannot have its\nsave\n                 subtracted at parse time.  Replaces the FIXME.\n    -            (time_zone::_Impl::_M_get_sys_info): Change the seeding\nactive-\n    +            (time_zone::_M_get_sys_info): Change the seeding active-\n                 rule lookup to use t = info.begin + 1s, so a rule firing at\n                 exactly info.begin is included.\n                 (reload_tzdb): After sorting node->rules, run a fixup pass\nover\n    @@ Commit message\n                 canonical +10h boundary is now produced.\n                 * testsuite/std/time/time_zone/pr116110_named.cc: New test.\n\n    +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>\n    +\n      ## libstdc++-v3/src/c++20/tzdb.cc ##\n     @@ libstdc++-v3/src/c++20/tzdb.cc: 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\nnamed-Rule\n    -+      // line, the SAVE component of the conversion can't be applied\nyet\n    -+      // because the active Rule depends on the cumulative state of\nthe\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\nall\n    -+      // Rule records are loaded to subtract the SAVE.\n    ++      // True if this is a named-rule zone line whose wall-time UNTIL\nstill\n    ++      // needs its SAVE adjustment applied.  See reload_tzdb for the\nfixup.\n     +      bool\n     +      until_save_pending() const noexcept { return\nm_until_save_pending; }\n     +\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     +      void\n     +      clear_until_save_pending() noexcept { m_until_save_pending = 0;\n}\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    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\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\nabove\n    ++      uint_least16_t m_until_save_pending : 1 = 0;\n            duration<int_least16_t, ratio<60>> m_save{};\n            sec32_t m_offset{};\n            sys_seconds m_until{};\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     +\n     +    // Find the Rule whose save value is in force at the wall-time\nUNTIL\n     +    // of a Zone line, given that `wall_minus_stdoff` is the line's\nUNTIL\n    -+    // expressed in the \"save=0\" frame (i.e. the parsed wall UNTIL\nwith\n    -+    // the line's STDOFF subtracted) and `stdoff` is the line's\nstandard\n    -+    // offset.\n    -+    //\n    -+    // The function walks all (rule, year) pairs in chronological\norder,\n    -+    // maintaining a running save value.  Wall-time rules have their\nTIME\n    -+    // field interpreted relative to the running save (since \"wall\"\nmeans\n    -+    // local civil time = stdoff + save), so a rule's effective UT\nfiring\n    -+    // time depends on which prior rule was last in force.  This\nmatches\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    ++    // with STDOFF subtracted and `stdoff` is the line's standard\noffset.\n     +    //\n    -+    // The comparison `fire < boundary` shrinks `boundary` as the\nrunning\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    ++    // Walks (rule, year) pairs chronologically, maintaining a running\n    ++    // save value used to interpret subsequent Wall-indicator rules.\n    ++    // The boundary `wall_minus_stdoff - running_save` shrinks as save\n    ++    // accumulates, so a rule firing AT the boundary is treated as\n    ++    // belonging to the next zone line.\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\nUNTIL\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    ++    // The calendar window extends by one year on each side to catch\n    ++    // rules whose wall at_time crosses a year boundary in UT due to a\n    ++    // large stdoff or save.\n     +    template<typename _RuleRange>\n     +      const Rule*\n     +      find_pre_until_rule(const _RuleRange& rules,\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n          if (letters.empty())\n            {\n     - sys_seconds t = info.begin - seconds(1);\n    -+ // We want the rule whose effect is in force at info.begin --\n    -+ // including a rule that fires at exactly info.begin (its effect\n    -+ // has just begun and is active for the first sys_info we are\n    -+ // about to generate).  The search below uses a strict\n    -+ // `rule_start < t` comparison, so pass info.begin + 1s to make\n    -+ // the half-open lookup (..., info.begin] inclusive of the\n    -+ // boundary instant.  This is what makes named-rule zone lines\n    -+ // like Africa/Algiers (PR 116110) seed with the correct save:\n    -+ // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's\n    -+ // frame, which is exactly the new line's begin.\n    ++ // info.begin + 1s makes the strict `rule_start < t` search\n    ++ // inclusive of a rule that fires at exactly info.begin.\n     + sys_seconds t = info.begin + seconds(1);\n      const year_month_day date(chrono::floor<days>(t));\n\n    @@ libstdc++-v3/src/c++20/tzdb.cc: 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\nwall-\n    -+    // time expression on a named-rule line, the parser deferred the\nSAVE\n    -+    // adjustment because the active rule wasn't yet identifiable.\nNow\n    -+    // that all Rule records are loaded and indexed, walk every\npending\n    -+    // ZoneInfo, find the rule whose effect was in force just before\nthe\n    -+    // wall UNTIL, and subtract that rule's save from m_until.\n    -+    //\n    -+    // \"Just before the wall UNTIL\" matches zic.c's interpretation:\nthe\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\nat\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\nzone\n    -+    // line, not this one.  find_pre_until_rule's iterative-boundary\n    -+    // walker implements this semantics.\n    ++    // For every Zone line whose UNTIL was a wall-time expression on a\n    ++    // named-rule line, the parser deferred the SAVE adjustment\nbecause\n    ++    // the active rule was not yet identifiable.  Now that all Rule\n    ++    // records are loaded and indexed, find the rule active just\nbefore\n    ++    // the wall UNTIL and subtract its save from m_until.\n     +    for (const auto& tz : node->db.zones)\n     +      {\n     + auto& infos = tz._M_impl->infos;\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n         inf.m_until -= inf.m_save;\n     -  // else Named Rule, SAVE is unknown. FIXME: PR 116110\n     +  else\n    -+    // Named Rule: SAVE depends on which rule of the set\n    -+    // was active at this instant, which can only be\n    -+    // determined once all Rule records are loaded.  Mark\n    -+    // the ZoneInfo so that the fixup pass in reload_tzdb\n    -+    // applies the deferred adjustment.  PR 116110.\n    ++    // Named Rule: defer SAVE adjustment until reload_tzdb\n    ++    // has loaded all Rule records.\n     +    inf.set_until_save_pending();\n      }\n         }\n    @@ libstdc++-v3/testsuite/std/time/time_zone/116110.cc: test_apia()\n\n     -  // FIXME: this should be + 10h but we do not account for DST yet,\nso + 11h.\n     -  sys_seconds ut(t.time_since_epoch() + 11h );\n    -+  // The wall UNTIL \"2011 Dec 29 24\" is interpreted in the prior\noffset\n    -+  // (-11h + save 1h = -10h), so the boundary is at local_days +24h\n+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    ++  // The wall UNTIL is interpreted in the prior offset (-11h + save 1h\n    ++  // = -10h), so the boundary is at local_days + 24h + 10h.\n     +  sys_seconds ut(t.time_since_epoch() + 10h );\n        sys_info info;\n        info = tz->get_info(ut - 1s);\n    @@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)\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    ++// Africa/Algiers 1977-10-21: a Zone line whose RULES references a\n    ++// named Rule and whose UNTIL is a wall-time expression.  The wall\n    ++// UNTIL is interpreted using the SAVE value in force just before the\n    ++// boundary (the May-6 rule's save=1, not the Oct-21 rule's save=0\n    ++// even though the Oct-21 rule fires at the same wall instant).\n     +//\n    -+// A Zone line whose RULES references a named Rule and whose UNTIL is\na\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\non\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\nloaded\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\nfires\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\ninstant\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    @@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)\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    ++  // The boundary is Oct 20 23:00 UTC (= wall 00:00 - stdoff(0) -\nsave(1)).\n    ++  // At and after the boundary we are in the 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\nline)\n     +  VERIFY( at.save == 0min );\n    @@ libstdc++-v3/testsuite/std/time/time_zone/pr116110_named.cc (new)\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    ++  // A query inside the [Oct 20 23:00, Oct 21 00:00] UTC window must\nbe\n    ++  // in the second line, not in a leftover stretch from the first\nline.\n     +  auto window = tz->get_info(sys_days{1977y/October/20} + 23h +\n30min);\n     +  VERIFY( window.offset == 1h );\n     +  VERIFY( window.abbrev == \"CET\" );\n4:  d94019806 ! 4:  9a01b2555 libstdc++: Cascade wall-time saves in lazy\nexpansion seeding [PR 124853]\n    @@\n      ## Metadata ##\n    -Author: Alvaro Begue <alvaro.begue@gmail.com>\n    +Author: Álvaro Begué <alvaro.begue@gmail.com>\n\n      ## Commit message ##\n    -    libstdc++: Cascade wall-time saves in lazy expansion seeding [PR\n124853]\n    +    libstdc++: Cascade wall-time saves in lazy expansion seeding\n[PR124853]\n\n         When _M_get_sys_info seeds a Zone line by looking up the active\nrule\n         just before info.begin, the previous code interpreted each rule in\n    @@ Commit message\n         libstdc++-v3/ChangeLog:\n\n                 PR libstdc++/124853\n    -            * src/c++20/tzdb.cc (time_zone::_Impl::_M_get_sys_info):\n    +            * src/c++20/tzdb.cc (time_zone::_M_get_sys_info):\n                 Replace the per-rule isolated active-rule search with a\n                 chronological cascade walker that maintains a running save\n                 and interprets Wall-time rules' at_time relative to it.\n    @@ Commit message\n                 \"earliest STD rule\" fallback to its own branch.\n                 * testsuite/std/time/time_zone/wall_cascade.cc: New test.\n\n    +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>\n    +\n      ## libstdc++-v3/src/c++20/tzdb.cc ##\n     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n      #endif\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n\n     -    // Find the Rule whose save value is in force at the wall-time\nUNTIL\n     -    // of a Zone line, given that `wall_minus_stdoff` is the line's\nUNTIL\n    --    // expressed in the \"save=0\" frame (i.e. the parsed wall UNTIL\nwith\n    --    // the line's STDOFF subtracted) and `stdoff` is the line's\nstandard\n    --    // offset.\n    -+    // Find the Rule in `rules` whose effect was last in force at time\n    -+    // `t`, given that `stdoff` is the standard offset of the\nenclosing\n    -+    // zone line.  Returns nullptr if no rule fired strictly before t.\n    +-    // with STDOFF subtracted and `stdoff` is the line's standard\noffset.\n    ++    // Find the Rule whose effect was last in force at time `t`, given\n    ++    // that `stdoff` is the standard offset of the enclosing zone\nline.\n    ++    // Returns nullptr if no rule fired strictly before t.\n          //\n    -     // The function walks all (rule, year) pairs in chronological\norder,\n    -     // maintaining a running save value.  Wall-time rules have their\nTIME\n    -@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\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    +     // Walks (rule, year) pairs chronologically, maintaining a running\n    +     // save value used to interpret subsequent Wall-indicator rules.\n    +-    // The boundary `wall_minus_stdoff - running_save` shrinks as save\n    +-    // accumulates, so a rule firing AT the boundary is treated as\n    +-    // belonging to the next zone line.\n          //\n    -+    // Canonical case: Europe/Paris around 1945, where the France\nrules\n    -+    //   1945 Apr 2  02:00 wall  save=2  M\n    -+    //   1945 Sep 16 03:00 wall  save=0  -\n    -+    // chain together: in the (stdoff=1, save=2) frame the September\n    -+    // rule fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.\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 (Pacific/Auckland's \"1946 Ja 1\" rule\n    -+    // in stdoff=12h fires at 1945-12-31 11:30 UT).\n    +     // The calendar window extends by one year on each side to catch\n    +     // rules whose wall at_time crosses a year boundary in UT due to a\n    +     // large stdoff or save.\n     +    template<typename _RuleRange>\n     +      const Rule*\n     +      find_active_rule(const _RuleRange& rules, sys_seconds t,\nseconds stdoff)\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     +\n     +    // Find the Rule whose save value is in force at the wall-time\nUNTIL\n     +    // of a Zone line, given that `wall_minus_stdoff` is the line's\nUNTIL\n    -+    // expressed in the \"save=0\" frame (i.e. the parsed wall UNTIL\nwith\n    -+    // the line's STDOFF subtracted) and `stdoff` is the line's\nstandard\n    -+    // offset.\n    ++    // with STDOFF subtracted and `stdoff` is the line's standard\noffset.\n     +    //\n    -+    // Like find_active_rule, walks (rule, year) pairs chronologically\n    -+    // with a running save value, but interprets `t` differently.\n    -+    //\n    -     // The comparison `fire < boundary` shrinks `boundary` as the\nrunning\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    -@@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\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    ++    // Like find_active_rule but `wall_minus_stdoff - running_save`\n    ++    // shrinks as save accumulates, so a rule firing AT the boundary\nis\n    ++    // treated as belonging to the next zone line.\n          template<typename _RuleRange>\n            const Rule*\n            find_pre_until_rule(const _RuleRange& rules,\n     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n    - // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's\n    - // frame, which is exactly the new line's begin.\n    + // info.begin + 1s makes the strict `rule_start < t` search\n    + // inclusive of a rule that fires at exactly info.begin.\n      sys_seconds t = info.begin + seconds(1);\n     - const year_month_day date(chrono::floor<days>(t));\n\n    @@ libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc (new)\n     +// { dg-require-effective-target cxx11_abi }\n     +// { dg-xfail-run-if \"no weak override on AIX\" { powerpc-ibm-aix* } }\n     +\n    -+// Regression test: when lazy expansion seeds a Zone line whose rule\nset\n    -+// has wall-time rules whose effective firing time depends on a prior\n    -+// rule's save (cascading wall-time interpretation), the seeding code\n    -+// must walk the rules chronologically with a running save value, not\n    -+// look at each rule in isolation against a fixed `save=0` frame.\n    -+//\n    -+// Mirrors the Europe/Paris 1945 case.  France's rules\n    ++// Wall-time rules in the same rule set whose effective firing time\n    ++// depends on a prior rule's save (Europe/Paris 1945):\n     +//   1945 Apr 2  02:00 wall  save=2  M\n     +//   1945 Sep 16 03:00 wall  save=0  -\n    -+// chain together: in the (stdoff=1, save=2) frame the September rule\n    -+// fires at Sep 16 00:00 UT, not Sep 16 02:00 UT.\n    -+//\n    -+// Construct a synthetic two-line zone whose second line begins\nbetween\n    -+// those two interpretations of the September rule, so the seeding has\n    -+// to choose: with the cascade, the September rule has already fired\nat\n    -+// info.begin and the new line seeds with save=0; without the cascade,\n    -+// the September rule appears not to have fired yet and the new line\n    -+// would (incorrectly) seed with save=2 from the April rule.\n    ++// In the (stdoff=1, save=2) frame the September rule fires at\n    ++// Sep 16 00:00 UT, not Sep 16 02:00 UT.\n     +\n     +#include <chrono>\n     +#include <fstream>\n    @@ libstdc++-v3/testsuite/std/time/time_zone/wall_cascade.cc (new)\n     +  // Line 1 ends at \"1945 Sep 16 1u\" (Universal time, no save\nshenanigans),\n     +  // so info.begin for line 2 is exactly 1945-09-16 01:00 UT.\n     +  //\n    -+  // Cascade seeding: in line 2's frame (stdoff=1):\n    -+  //   * Apr 2 fires at Apr 2 01:00 UT (running save 0 → 2)\n    -+  //   * Sep 16 fires at Sep 16 00:00 UT (running save 2 → 0)\n    -+  // Both fire before info.begin (Sep 16 01:00 UT), so the active rule\n    -+  // at info.begin is the Sep 16 one, save=0 → CET, total offset 1h.\n    -+  //\n    -+  // Without cascade: Sep 16 is computed as 03:00 - stdoff(1) = 02:00\nUT,\n    -+  // which is *after* info.begin, so the seeding falls back to the\nApril\n    -+  // rule's save=2 → CEMT, total offset 3h.  That is the bug.\n    ++  // Two-line zone whose second line begins at 1945 Sep 16 01:00 UT,\n    ++  // between the cascaded firing time (Sep 16 00:00 UT) and the\n    ++  // non-cascaded firing time (Sep 16 02:00 UT) of the September rule.\n    ++  // The seeding must pick the September rule (save=0, CET) at\ninfo.begin.\n     +  std::ofstream(\"tzdata.zi\") << R\"(# version test_wall_cascade\n     +R Fr 1945 o - Apr 2  2 2 M\n     +R Fr 1945 o - Sep 16 3 0 -\n5:  ab7938201 ! 5:  1c717d45a libstdc++: Implement zic writezone merge\noptimization [PR 124854]\n    @@\n      ## Metadata ##\n    -Author: Alvaro Begue <alvaro.begue@gmail.com>\n    +Author: Álvaro Begué <alvaro.begue@gmail.com>\n\n      ## Commit message ##\n    -    libstdc++: Implement zic writezone merge optimization [PR 124854]\n    +    libstdc++: Implement zic writezone merge optimization [PR124854]\n\n         Two distinct correctness fixes that together let lazy expansion\nmatch\n         zic.c's writezone output for zones with rule firings near zone-line\n    @@ Commit message\n         libstdc++-v3/ChangeLog:\n\n                 PR libstdc++/124854\n    -            * src/c++20/tzdb.cc (time_zone::_Impl::_M_get_sys_info):\n    +            * src/c++20/tzdb.cc (time_zone::_M_get_sys_info):\n                 Always run find_active_rule to seed info.offset and\ninfo.save,\n                 regardless of whether letters was already populated from\n                 i[-1].next_letters().  Add a writezone merge optimization\n    @@ Commit message\n                 re-entry.\n                 * testsuite/std/time/time_zone/zone_merge.cc: New test.\n\n    +    Signed-off-by: Álvaro Begué <alvaro.begue@gmail.com>\n    +\n      ## libstdc++-v3/src/c++20/tzdb.cc ##\n     @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n          if (i != infos.begin() && i[-1].expanded())\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n\n     -    if (letters.empty())\n     -      {\n    -- // We want the rule whose effect is in force at info.begin --\n    -- // including a rule that fires at exactly info.begin (its effect\n    -- // has just begun and is active for the first sys_info we are\n    -- // about to generate).  The search below uses a strict\n    -- // `rule_start < t` comparison, so pass info.begin + 1s to make\n    -- // the half-open lookup (..., info.begin] inclusive of the\n    -- // boundary instant.  This is what makes named-rule zone lines\n    -- // like Africa/Algiers (PR 116110) seed with the correct save:\n    -- // the Oct-21 rule fires at Oct 20 23:00 UTC in the new line's\n    -- // frame, which is exactly the new line's begin.\n    +- // info.begin + 1s makes the strict `rule_start < t` search\n    +- // inclusive of a rule that fires at exactly info.begin.\n     - sys_seconds t = info.begin + seconds(1);\n     -\n     - // Try to find a Rule active before this time, to get initial\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     -  {\n     -    info.offset = ri.offset() + active_rule->save;\n     -    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    ++    // Seed info.offset and info.save from the rule active at\n    ++    // info.begin.  Always run this (even when `letters` was populated\n    ++    // from i[-1].next_letters() during partial-expansion re-entry),\n    ++    // because info.offset/save are still at their stdoff/0 init\nvalues\n    ++    // and would otherwise produce 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    ++    // info.begin + 1s makes the strict `rule_start < t` search\n    ++    // inclusive of a rule that fires at exactly info.begin.\n     +    {\n     +      sys_seconds t = info.begin + seconds(1);\n     +      const Rule* active_rule = find_active_rule(rules, t,\nri.offset());\n    @@ libstdc++-v3/src/c++20/tzdb.cc: namespace std::chrono\n     + }\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    ++    // zic.c writezone merge optimization.  When the local time jumps\n    ++    // backward at a zone-line boundary and a rule in the new line's\nset\n    ++    // fires within that gap window, fold the rule's save into the\n    ++    // boundary so the new line begins with the post-rule save.\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    ++    // Only runs at the first sys_info of a zone line (not on partial-\n    ++    // expansion re-entry mid-line): a mid-line re-entry's prior\n    ++    // ZoneInfo has a non-empty next_letters(), while a zone-line\n    ++    // transition's prior ZoneInfo ends with empty next_letters().\n     +    if (i != infos.begin() && i[-1].expanded()\n     +  && i[-1].next_letters().empty())\n     +      {\n    @@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)\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\nwith\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,\ni.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\npost-rule\n    -+// save rather than briefly using the pre-rule save and then\ntransitioning\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    ++// When two adjacent Zone lines differ in total offset and the new\nline's\n    ++// rule set has a rule firing within |jump| of the boundary (where\njump\n    ++// is a backward local-time jump), zic.c's writezone folds that rule\n    ++// into the boundary, so the new line begins with the post-rule save.\n     +//\n    -+// Mirror the Buenos Aires shape with a synthetic zone.\n    ++// Mirrors America/Argentina/Buenos_Aires around 1999-10-03.\n     +\n     +#include <chrono>\n     +#include <fstream>\n    @@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)\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    ++  // stdoff jumps from -3 to -4 at the same instant a save=1 rule\nfires.\n    ++  // In the new (-4) frame the rule fires 1 hour after the boundary at\n    ++  // UT 03:00, so the merge folds the rule into the boundary and the\n    ++  // new line begins at offset=-3, save=1 (abbrev \"-03\").\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    @@ libstdc++-v3/testsuite/std/time/time_zone/zone_merge.cc (new)\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    ++  // The new line's first sys_info already has save=1 from the merge,\n    ++  // total offset -3h, abbrev \"-03\".\n     +  auto at_boundary = tz->get_info(boundary);\n     +  VERIFY( at_boundary.offset == -3h );\n     +  VERIFY( at_boundary.save == 60min );"}