From patchwork Mon Jul 29 17:56:06 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jonathan Wakely X-Patchwork-Id: 1966181 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@legolas.ozlabs.org Authentication-Results: legolas.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256 header.s=mimecast20190719 header.b=hU7klqQ0; dkim-atps=neutral Authentication-Results: legolas.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=gcc.gnu.org (client-ip=8.43.85.97; helo=server2.sourceware.org; envelope-from=gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org; receiver=patchwork.ozlabs.org) Received: from server2.sourceware.org (server2.sourceware.org [8.43.85.97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (secp384r1) server-digest SHA384) (No client certificate requested) by legolas.ozlabs.org (Postfix) with ESMTPS id 4WXmMT216Dz1ybX for ; Tue, 30 Jul 2024 03:59:17 +1000 (AEST) Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 84A95385DC3C for ; Mon, 29 Jul 2024 17:59:15 +0000 (GMT) X-Original-To: gcc-patches@gcc.gnu.org Delivered-To: gcc-patches@gcc.gnu.org Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) by sourceware.org (Postfix) with ESMTP id C5A3A3858283 for ; Mon, 29 Jul 2024 17:58:42 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org C5A3A3858283 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=redhat.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=redhat.com ARC-Filter: OpenARC Filter v1.0.0 sourceware.org C5A3A3858283 Authentication-Results: server2.sourceware.org; arc=none smtp.remote-ip=170.10.133.124 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1722275926; cv=none; b=wZd5ZeqOIWsZw48sbgHm6JHEBssTO2lMgXANgrSdTLqgI2N7k/QYhjVxywNkPLFAqbz5EySAkyJVWrI1bNI6w6Twjo9W0PFHNG2xxZhnsgEAqevYY1QAp4fdXdpSdNUMxpqeE4MZAEQxmkzpqbRyIwsYVB0j/T7yXRwVSpcm0b4= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1722275926; c=relaxed/simple; bh=ccIzI5WxCkQEN4LYpYcx0OqZApY87/zgaaTgsBJpizw=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=aXpuHvjzjJ6SwQEIu8Bp+It+49Kpxz2llIDvXzz942JPzvS0+/CIbOsX5cFTKlSVDz4MLcUKJh3RRPshIG/OrUGsIf7dyHuVS7MB6cY2+BVgPvOXX8s/24wnpeBBAiDGr39kAjpY+HsBX4q0Httn2VK64sP7KXzOWvonozDvme0= ARC-Authentication-Results: i=1; server2.sourceware.org DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1722275922; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=M26t/6ank9Y9BDsIJ+Sz4nwjVUJuhSKdA+82yMXiT4g=; b=hU7klqQ0yfrEMlYYGf8d6KaLOT7qqmuAYss61vU6c8REM7LbPnGBUTh9rrxmVydBUv0FV6 jfFetGfnFcl2+YVDVXtnOxbZHOp5ZDJey8IUDJQo7mNQ+rdsmcIF3jPWk770ZOan2esdzO 55s9AQSTcE7Gs0qhTr0ZNimEwoOSFMI= Received: from mx-prod-mc-02.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-480-IY_6SgKgP0aP6ssUSShqaA-1; Mon, 29 Jul 2024 13:58:39 -0400 X-MC-Unique: IY_6SgKgP0aP6ssUSShqaA-1 Received: from mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.12]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-02.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 4D8571955D55; Mon, 29 Jul 2024 17:58:38 +0000 (UTC) Received: from localhost (unknown [10.42.28.14]) by mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id A9A0819560AE; Mon, 29 Jul 2024 17:58:36 +0000 (UTC) From: Jonathan Wakely To: libstdc++@gcc.gnu.org, gcc-patches@gcc.gnu.org Subject: [PATCH 1/2] libstdc++: Fix std::format output for std::chrono::zoned_time Date: Mon, 29 Jul 2024 18:56:06 +0100 Message-ID: <20240729175835.825721-1-jwakely@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.0 on 10.30.177.12 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com X-Spam-Status: No, score=-11.1 required=5.0 tests=BAYES_00, DKIMWL_WL_HIGH, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, LIKELY_SPAM_BODY, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H4, RCVD_IN_MSPIKE_WL, SPF_HELO_NONE, SPF_NONE, TXREP autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org X-BeenThere: gcc-patches@gcc.gnu.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Gcc-patches mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: gcc-patches-bounces~incoming=patchwork.ozlabs.org@gcc.gnu.org My first attempt to fix this was an overly complex kluge, but there was a nice simple solution staring me in the face. I'm pretty happy with this now. Tested x86_64-linux. -- >8 -- When formatting a chrono::zoned_time with an empty chrono-specs, we were only formatting its _M_time member, but the ostream insertion operator uses the format "{:L%F %T %Z}" which includes the time zone abbreviation. The %Z should also be used when formatting with an empty chrono-specs. This commit makes _M_format_to_ostream handle __local_time_fmt specializations directly, rather than calling itself recursively to format the _M_time member. We need to be able to customize the output of _M_format_to_ostream for __local_time_fmt, because we use that type for gps_time and tai_time as well as for zoned_time and __local_time_fmt. When formatting gps_time and tai_time we don't want to include the time zone abbreviation in the "{}" output, but for zoned_time we do want to. We can reuse the __is_neg flag passed to _M_format_to_ostream (via _M_format) to say that we want the time zone abbreviation. Currently the __is_neg flag is only used for duration specializations, so it's available for __local_time_fmt to use. In addition to fixing the zoned_time output to use %Z, this commit also changes the __local_time_fmt output to use %Z. Previously it didn't use it, just like zoned_time. The standard doesn't actually say how to format local-time-format-t for an empty chrono-specs, but this behaviour seems sensible and is what I'm proposing as part of LWG 4124. While testing this I noticed that some chrono types were not being tested with empty chrono-specs, so this adds more tests. I also noticed that std/time/clock/local/io.cc was testing tai_time instead of local_time, which was completely wrong. That's fixed now too. libstdc++-v3/ChangeLog: * include/bits/chrono_io.h (__local_fmt_t): Remove unused declaration. (__formatter_chrono::_M_format_to_ostream): Add explicit handling for specializations of __local_time_fmt, including the time zone abbreviation in the output if __is_neg is true. (formatter>::format): Add comment. (formatter>::format): Likewise. (formatter struct __local_time_fmt { @@ -163,8 +164,6 @@ namespace __detail const string* _M_abbrev; const seconds* _M_offset_sec; }; - - struct __local_fmt_t; } /// @endcond @@ -695,13 +694,34 @@ namespace __format using ::std::chrono::__detail::__utc_leap_second; using ::std::chrono::__detail::__local_time_fmt; + basic_ostringstream<_CharT> __os; + __os.imbue(_M_locale(__fc)); + if constexpr (__is_specialization_of<_Tp, __local_time_fmt>) - return _M_format_to_ostream(__t._M_time, __fc, false); + { + // Format as "{:L%F %T}" + auto __days = chrono::floor(__t._M_time); + __os << chrono::year_month_day(__days) << ' ' + << chrono::hh_mm_ss(__t._M_time - __days); + + // For __local_time_fmt the __is_neg flags says whether to + // append " %Z" to the result. + if (__is_neg) + { + if (!__t._M_abbrev) [[unlikely]] + __format::__no_timezone_available(); + else if constexpr (is_same_v<_CharT, char>) + __os << ' ' << *__t._M_abbrev; + else + { + __os << L' '; + for (char __c : *__t._M_abbrev) + __os << __c; + } + } + } else { - basic_ostringstream<_CharT> __os; - __os.imbue(_M_locale(__fc)); - if constexpr (__is_specialization_of<_Tp, __utc_leap_second>) __os << __t._M_date << ' ' << __t._M_time; else if constexpr (chrono::__is_time_point_v<_Tp>) @@ -727,11 +747,11 @@ namespace __format __os << _S_plus_minus[1]; __os << __t; } - - auto __str = std::move(__os).str(); - return __format::__write_padded_as_spec(__str, __str.size(), - __fc, _M_spec); } + + auto __str = std::move(__os).str(); + return __format::__write_padded_as_spec(__str, __str.size(), + __fc, _M_spec); } static constexpr const _CharT* _S_chars @@ -2008,6 +2028,8 @@ namespace __format _FormatContext& __fc) const { // Convert to __local_time_fmt with abbrev "TAI" and offset 0s. + // We use __local_time_fmt and not sys_time (as the standard implies) + // because %Z for sys_time would print "UTC" and we want "TAI" here. // Offset is 1970y/January/1 - 1958y/January/1 constexpr chrono::days __tai_offset = chrono::days(4383); @@ -2038,6 +2060,8 @@ namespace __format _FormatContext& __fc) const { // Convert to __local_time_fmt with abbrev "GPS" and offset 0s. + // We use __local_time_fmt and not sys_time (as the standard implies) + // because %Z for sys_time would print "UTC" and we want "GPS" here. // Offset is 1980y/January/Sunday[1] - 1970y/January/1 constexpr chrono::days __gps_offset = chrono::days(3657); @@ -2104,7 +2128,7 @@ namespace __format typename _FormatContext::iterator format(const chrono::__detail::__local_time_fmt<_Duration>& __t, _FormatContext& __ctx) const - { return _M_f._M_format(__t, __ctx); } + { return _M_f._M_format(__t, __ctx, /* use %Z for {} */ true); } private: __format::__formatter_chrono<_CharT> _M_f; diff --git a/libstdc++-v3/testsuite/std/time/clock/gps/io.cc b/libstdc++-v3/testsuite/std/time/clock/gps/io.cc index d5405f61520..e995d9f3d78 100644 --- a/libstdc++-v3/testsuite/std/time/clock/gps/io.cc +++ b/libstdc++-v3/testsuite/std/time/clock/gps/io.cc @@ -22,7 +22,6 @@ test_ostream() ss << gt; VERIFY( ss.str() == "2000-01-01 00:00:13" ); - gps_time> gtf = gt; ss.str(""); ss.clear(); ss << (gps_time>(gt) + 20ms); diff --git a/libstdc++-v3/testsuite/std/time/clock/local/io.cc b/libstdc++-v3/testsuite/std/time/clock/local/io.cc index bb682cd40cf..b6e8b355bd0 100644 --- a/libstdc++-v3/testsuite/std/time/clock/local/io.cc +++ b/libstdc++-v3/testsuite/std/time/clock/local/io.cc @@ -7,15 +7,85 @@ void test_ostream() +{ + using namespace std::chrono; + + auto lt = local_time(1722198122s); + + std::ostringstream ss; + ss << lt; + auto s = ss.str(); + ss.str(""); + ss.clear(); + ss << sys_time{lt.time_since_epoch()}; + auto s2 = ss.str(); + VERIFY( s == s2 ); +} + +void +test_format() { using std::format; using namespace std::chrono; - auto st = sys_days{2000y/January/1}; - auto tt = clock_cast(st); + auto lt = local_seconds(1722198122s); - auto s = format("{0:%F %T %Z} == {1:%F %T %Z}", st, tt); - VERIFY( s == "2000-01-01 00:00:00 UTC == 2000-01-01 00:00:32 TAI" ); +#if __cpp_exceptions + auto args = std::make_format_args(lt); + try + { + (void) std::vformat("{:%Z}", args); + VERIFY(false); + } + catch (const std::format_error&) + { + } + try + { + (void) std::vformat("{:%z}", args); + VERIFY(false); + } + catch (const std::format_error&) + { + } +#endif + + auto s = format("{0:%F %T %a}", lt); + VERIFY( s == "2024-07-28 20:22:02 Sun" ); + + s = format("{}", lt); + VERIFY( s == "2024-07-28 20:22:02" ); + + // Test formatting for chrono::local_time_format and local-time-format-t too: + auto ltf = local_time_format(lt); + s = std::format("{:%F %T %a %b}", ltf); + VERIFY( s == "2024-07-28 20:22:02 Sun Jul" ); +#if __cpp_exceptions + try + { + (void) std::format("{:%Z}", ltf); + VERIFY(false); + } + catch (const std::format_error&) + { + } + try + { + (void) std::format("{:%z}", ltf); + VERIFY(false); + } + catch (const std::format_error&) + { + } +#endif + std::string abbrev = "FOO"; + seconds off = -3600s; + ltf = local_time_format(lt, &abbrev, &off); + s = std::format("{}", ltf); + VERIFY( s == "2024-07-28 20:22:02 FOO" ); + s = std::format("{:%Z %T %F %z %Ez}", ltf); + __builtin_puts(s.c_str()); + VERIFY( s == "FOO 20:22:02 2024-07-28 -0100 -01:00" ); } void diff --git a/libstdc++-v3/testsuite/std/time/clock/system/io.cc b/libstdc++-v3/testsuite/std/time/clock/system/io.cc index 2cc116156f2..5c223a979c2 100644 --- a/libstdc++-v3/testsuite/std/time/clock/system/io.cc +++ b/libstdc++-v3/testsuite/std/time/clock/system/io.cc @@ -64,13 +64,19 @@ test_format() " | 25.708 | 17:26:25.708 | 1 | 51 | 51 | 1 | 51 | 12/19/22" " | 17:26:25 | 22 | 2022 | +0000 | UTC" ); + auto st = std::chrono::time_point_cast(t); auto loc = std::locale::classic(); auto smod = std::format(loc, "{:%Ec %EC %Od %Oe %OH %OI %Om %OM %OS %Ou %OU" " %Ow %OW %Ex %EX %Oy %Ey %EY %Ez %Oz}", t); s = std::format("{:%c %C %d %e %H %I %m %M %S %u %U" " %w %W %x %X %y %y %Y +00:00 +00:00}", - std::chrono::time_point_cast(t)); + st); VERIFY( smod == s ); + + s = std::format("{}", t); + VERIFY( s == "2022-12-19 17:26:25.708" ); + s = std::format("{0:} {0:=<21}", st); + VERIFY( s == "2022-12-19 17:26:25 2022-12-19 17:26:25==" ); } void diff --git a/libstdc++-v3/testsuite/std/time/clock/tai/io.cc b/libstdc++-v3/testsuite/std/time/clock/tai/io.cc index 0fd61c0e612..dc0c3d26477 100644 --- a/libstdc++-v3/testsuite/std/time/clock/tai/io.cc +++ b/libstdc++-v3/testsuite/std/time/clock/tai/io.cc @@ -16,6 +16,9 @@ test_ostream() auto s = format("{0:%F %T %Z} == {1:%F %T %Z}", st, tt); VERIFY( s == "2000-01-01 00:00:00 UTC == 2000-01-01 00:00:32 TAI" ); + + s = std::format("{:=>21}", tt); + VERIFY( s == "==2000-01-01 00:00:32" ); } void diff --git a/libstdc++-v3/testsuite/std/time/zoned_time/io.cc b/libstdc++-v3/testsuite/std/time/zoned_time/io.cc index 376b2734f19..ee3b9edba81 100644 --- a/libstdc++-v3/testsuite/std/time/zoned_time/io.cc +++ b/libstdc++-v3/testsuite/std/time/zoned_time/io.cc @@ -37,6 +37,7 @@ test_format() " | 25.708 | 12:26:25.708 | 1 | 51 | 51 | 1 | 51 | 12/19/22" " | 12:26:25 | 22 | 2022 | -0500 | EST" ); +#ifdef _GLIBCXX_USE_WCHAR_T std::wstring ws = std::format(L"{:%a | %A | %b | %B | %c" " | %C | %d | %D | %e | %F | %g | %G | %h" " | %H | %I | %j | %m | %M | %p | %r | %R" @@ -47,6 +48,7 @@ test_format() " | 12 | 12 | 353 | 12 | 26 | PM | 12:26:25 PM | 12:26" " | 25.708 | 12:26:25.708 | 1 | 51 | 51 | 1 | 51 | 12/19/22" " | 12:26:25 | 22 | 2022 | -0500 | EST" ); +#endif auto loc = std::locale::classic(); auto smod = std::format(loc, "{:%Ec %EC %Od %Oe %OH %OI %Om %OM %OS %Ou %OU" @@ -55,6 +57,15 @@ test_format() " %w %W %x %X %y %y %Y -05:00 -05:00}", zoned_time(zone, time_point_cast(t))); VERIFY( smod == s ); + + s = std::format("{}", zt); + VERIFY( s == "2022-12-19 12:26:25.708 EST" ); + s = std::format("{1:=>30}", 1, zt); + VERIFY( s == "===2022-12-19 12:26:25.708 EST" ); +#ifdef _GLIBCXX_USE_WCHAR_T + ws = std::format(L"{:+^34}", zoned_time(zone, t)); + VERIFY( ws == L"++2022-12-19 12:26:25.708000 EST++" ); +#endif } int main()