https://gcc.gnu.org/g:778c28c70f8573f8256c810ff0d9e143ff75dec3
commit r15-8491-g778c28c70f8573f8256c810ff0d9e143ff75dec3 Author: Jonathan Wakely <jwak...@redhat.com> Date: Wed Mar 19 22:05:28 2025 +0000 libstdc++: Fix localized %c formatting for non-UTC times [PR117214] The previous commit fixed most cases of %c formatting, but it incorrectly prints times using the system's local time zone. This only matters if the locale's %c format includes %Z, but some do. To print a correct value for %Z we can set tm.tm_zone to either "UTC" or the abbreviation passed to the formatter in the local-time-format-t structure. For local times with no info and for systems that don't support tm_zone (which is new in POSIX.1-2024) we just set tm_isdst = -1 so that no zone name is printed. In theory, a locale's %c format could use %z which should print a +hhmm offset from UTC. I'm unsure how to control that though. The new tm_gmtoff field in combination with tm_isdst != -1 seems like it should work, but using that without also setting tm_zone causes the system zone to be used for %Z again. That means local_time_format(lt, nullptr, &off) might work for a locale that uses %z but prints the wrong thing for %Z. This commit doesn't set tm_gmtoff even if _M_offset_sec is provided for a local-time-format-t value. libstdc++-v3/ChangeLog: PR libstdc++/117214 * configure.ac: Use AC_STRUCT_TIMEZONE. * config.h.in: Regenerate. * configure: Regenerate. * include/bits/chrono_io.h (__formatter_chrono::_M_c): Set tm_isdst and tm_zone. * testsuite/std/time/format/pr117214.cc: Check %c formatting of zoned_time and local time. Diff: --- libstdc++-v3/config.h.in | 18 +++ libstdc++-v3/configure | 179 ++++++++++++++++++++- libstdc++-v3/configure.ac | 2 + libstdc++-v3/include/bits/chrono_io.h | 32 +++- libstdc++-v3/testsuite/std/time/format/pr117214.cc | 65 ++++++++ 5 files changed, 287 insertions(+), 9 deletions(-) diff --git a/libstdc++-v3/config.h.in b/libstdc++-v3/config.h.in index 91e920044ee4..be151f43dd68 100644 --- a/libstdc++-v3/config.h.in +++ b/libstdc++-v3/config.h.in @@ -74,6 +74,10 @@ don't. */ #undef HAVE_DECL_STRNLEN +/* Define to 1 if you have the declaration of `tzname', and to 0 if you don't. + */ +#undef HAVE_DECL_TZNAME + /* Define to 1 if you have the <dirent.h> header file. */ #undef HAVE_DIRENT_H @@ -408,6 +412,9 @@ /* Define to 1 if `d_type' is a member of `struct dirent'. */ #undef HAVE_STRUCT_DIRENT_D_TYPE +/* Define to 1 if `tm_zone' is a member of `struct tm'. */ +#undef HAVE_STRUCT_TM_TM_ZONE + /* Define if strxfrm_l is available in <string.h>. */ #undef HAVE_STRXFRM_L @@ -499,9 +506,17 @@ /* Define to 1 if the target supports thread-local storage. */ #undef HAVE_TLS +/* Define to 1 if your `struct tm' has `tm_zone'. Deprecated, use + `HAVE_STRUCT_TM_TM_ZONE' instead. */ +#undef HAVE_TM_ZONE + /* Define if truncate is available in <unistd.h>. */ #undef HAVE_TRUNCATE +/* Define to 1 if you don't have `tm_zone' but do have the external array + `tzname'. */ +#undef HAVE_TZNAME + /* Define to 1 if you have the <uchar.h> header file. */ #undef HAVE_UCHAR_H @@ -590,6 +605,9 @@ /* Define to 1 if you have the ANSI C header files. */ #undef STDC_HEADERS +/* Define to 1 if your <sys/time.h> declares `struct tm'. */ +#undef TM_IN_SYS_TIME + /* Version number of package */ #undef VERSION diff --git a/libstdc++-v3/configure b/libstdc++-v3/configure index 78758285f211..67d2b8c7b72c 100755 --- a/libstdc++-v3/configure +++ b/libstdc++-v3/configure @@ -2731,6 +2731,63 @@ $as_echo "$ac_res" >&6; } eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno } # ac_fn_c_check_decl + +# ac_fn_c_check_member LINENO AGGR MEMBER VAR INCLUDES +# ---------------------------------------------------- +# Tries to find if the field MEMBER exists in type AGGR, after including +# INCLUDES, setting cache variable VAR accordingly. +ac_fn_c_check_member () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $2.$3" >&5 +$as_echo_n "checking for $2.$3... " >&6; } +if eval \${$4+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$5 +int +main () +{ +static $2 ac_aggr; +if (ac_aggr.$3) +return 0; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + eval "$4=yes" +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +$5 +int +main () +{ +static $2 ac_aggr; +if (sizeof ac_aggr.$3) +return 0; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + eval "$4=yes" +else + eval "$4=no" +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +eval ac_res=\$$4 + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +$as_echo "$ac_res" >&6; } + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + +} # ac_fn_c_check_member cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. @@ -12280,7 +12337,7 @@ else lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 lt_status=$lt_dlunknown cat > conftest.$ac_ext <<_LT_EOF -#line 12283 "configure" +#line 12340 "configure" #include "confdefs.h" #if HAVE_DLFCN_H @@ -12386,7 +12443,7 @@ else lt_dlunknown=0; lt_dlno_uscore=1; lt_dlneed_uscore=2 lt_status=$lt_dlunknown cat > conftest.$ac_ext <<_LT_EOF -#line 12389 "configure" +#line 12446 "configure" #include "confdefs.h" #if HAVE_DLFCN_H @@ -16182,7 +16239,7 @@ $as_echo "$glibcxx_cv_atomic_long_long" >&6; } # Fake what AC_TRY_COMPILE does. cat > conftest.$ac_ext << EOF -#line 16185 "configure" +#line 16242 "configure" int main() { typedef bool atomic_type; @@ -16217,7 +16274,7 @@ $as_echo "$glibcxx_cv_atomic_bool" >&6; } rm -f conftest* cat > conftest.$ac_ext << EOF -#line 16220 "configure" +#line 16277 "configure" int main() { typedef short atomic_type; @@ -16252,7 +16309,7 @@ $as_echo "$glibcxx_cv_atomic_short" >&6; } rm -f conftest* cat > conftest.$ac_ext << EOF -#line 16255 "configure" +#line 16312 "configure" int main() { // NB: _Atomic_word not necessarily int. @@ -16288,7 +16345,7 @@ $as_echo "$glibcxx_cv_atomic_int" >&6; } rm -f conftest* cat > conftest.$ac_ext << EOF -#line 16291 "configure" +#line 16348 "configure" int main() { typedef long long atomic_type; @@ -16444,7 +16501,7 @@ $as_echo "mutex" >&6; } # unnecessary for this test. cat > conftest.$ac_ext << EOF -#line 16447 "configure" +#line 16504 "configure" int main() { _Decimal32 d1; @@ -16486,7 +16543,7 @@ ac_compiler_gnu=$ac_cv_cxx_compiler_gnu # unnecessary for this test. cat > conftest.$ac_ext << EOF -#line 16489 "configure" +#line 16546 "configure" template<typename T1, typename T2> struct same { typedef T2 type; }; @@ -54640,6 +54697,112 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether struct tm is in sys/time.h or time.h" >&5 +$as_echo_n "checking whether struct tm is in sys/time.h or time.h... " >&6; } +if ${ac_cv_struct_tm+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include <sys/types.h> +#include <time.h> + +int +main () +{ +struct tm tm; + int *p = &tm.tm_sec; + return !p; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_struct_tm=time.h +else + ac_cv_struct_tm=sys/time.h +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_struct_tm" >&5 +$as_echo "$ac_cv_struct_tm" >&6; } +if test $ac_cv_struct_tm = sys/time.h; then + +$as_echo "#define TM_IN_SYS_TIME 1" >>confdefs.h + +fi + +ac_fn_c_check_member "$LINENO" "struct tm" "tm_zone" "ac_cv_member_struct_tm_tm_zone" "#include <sys/types.h> +#include <$ac_cv_struct_tm> + +" +if test "x$ac_cv_member_struct_tm_tm_zone" = xyes; then : + +cat >>confdefs.h <<_ACEOF +#define HAVE_STRUCT_TM_TM_ZONE 1 +_ACEOF + + +fi + +if test "$ac_cv_member_struct_tm_tm_zone" = yes; then + +$as_echo "#define HAVE_TM_ZONE 1" >>confdefs.h + +else + ac_fn_c_check_decl "$LINENO" "tzname" "ac_cv_have_decl_tzname" "#include <time.h> +" +if test "x$ac_cv_have_decl_tzname" = xyes; then : + ac_have_decl=1 +else + ac_have_decl=0 +fi + +cat >>confdefs.h <<_ACEOF +#define HAVE_DECL_TZNAME $ac_have_decl +_ACEOF + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for tzname" >&5 +$as_echo_n "checking for tzname... " >&6; } +if ${ac_cv_var_tzname+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test x$gcc_no_link = xyes; then + as_fn_error $? "Link tests are not allowed after GCC_NO_EXECUTABLES." "$LINENO" 5 +fi +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include <time.h> +#if !HAVE_DECL_TZNAME +extern char *tzname[]; +#endif + +int +main () +{ +return tzname[0][0]; + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_var_tzname=yes +else + ac_cv_var_tzname=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_var_tzname" >&5 +$as_echo "$ac_cv_var_tzname" >&6; } + if test $ac_cv_var_tzname = yes; then + +$as_echo "#define HAVE_TZNAME 1" >>confdefs.h + + fi +fi + + # Define documentation rules conditionally. # See if makeinfo has been installed and is modern enough diff --git a/libstdc++-v3/configure.ac b/libstdc++-v3/configure.ac index a3b257fe6528..fe0cdde1f7ac 100644 --- a/libstdc++-v3/configure.ac +++ b/libstdc++-v3/configure.ac @@ -584,6 +584,8 @@ GLIBCXX_CHECK_FILEBUF_NATIVE_HANDLES # For std::text_encoding GLIBCXX_CHECK_TEXT_ENCODING +AC_STRUCT_TIMEZONE + # Define documentation rules conditionally. # See if makeinfo has been installed and is modern enough diff --git a/libstdc++-v3/include/bits/chrono_io.h b/libstdc++-v3/include/bits/chrono_io.h index 86338d48d183..08969166d2f7 100644 --- a/libstdc++-v3/include/bits/chrono_io.h +++ b/libstdc++-v3/include/bits/chrono_io.h @@ -894,13 +894,42 @@ namespace __format // %Ec Locale's alternate date and time representation. using namespace chrono; + using ::std::chrono::__detail::__utc_leap_second; + using ::std::chrono::__detail::__local_time_fmt; + + struct tm __tm{}; + + // Some locales use %Z in their %c format but we don't want strftime + // to use the system's local time zone (from /etc/localtime or $TZ) + // as the output for %Z. Setting tm_isdst to -1 says there is no + // time zone info available for the time in __tm. + __tm.tm_isdst = -1; + +#ifdef _GLIBCXX_HAVE_STRUCT_TM_TM_ZONE + // POSIX.1-2024 adds tm.tm_zone which will be used for %Z. + if constexpr (__is_time_point_v<_Tp>) + { + // One of sys_time, utc_time, or local_time. + if constexpr (!is_same_v<typename _Tp::clock, local_t>) + __tm.tm_zone = "UTC"; + } + else if constexpr (__is_specialization_of<_Tp, __local_time_fmt>) + { + // local-time-format-t is used to provide time zone info for + // one of zoned_time, tai_time, gps_time, or local_time. + if (__t._M_abbrev) + __tm.tm_zone = __t._M_abbrev->c_str(); + } + else + __tm.tm_zone = "UTC"; +#endif + auto __d = _S_days(__t); // Either sys_days or local_days. using _TDays = decltype(__d); const year_month_day __ymd(__d); const auto __y = __ymd.year(); const auto __hms = _S_hms(__t); - struct tm __tm{}; __tm.tm_year = (int)__y - 1900; __tm.tm_yday = (__d - _TDays(__y/January/1)).count(); __tm.tm_mon = (unsigned)__ymd.month() - 1; @@ -909,6 +938,7 @@ namespace __format __tm.tm_hour = __hms.hours().count(); __tm.tm_min = __hms.minutes().count(); __tm.tm_sec = __hms.seconds().count(); + return _M_locale_fmt(std::move(__out), _M_locale(__ctx), __tm, 'c', __mod ? 'E' : '\0'); } diff --git a/libstdc++-v3/testsuite/std/time/format/pr117214.cc b/libstdc++-v3/testsuite/std/time/format/pr117214.cc index e78318322065..87c703dd2554 100644 --- a/libstdc++-v3/testsuite/std/time/format/pr117214.cc +++ b/libstdc++-v3/testsuite/std/time/format/pr117214.cc @@ -28,7 +28,72 @@ test_c() } } +#include <stdlib.h> +#include <time.h> + +extern "C++" int tzset(int); + +template<typename T = void> +void maybe_set_tz() +{ +#ifdef _GLIBCXX_HAVE_SETENV + // True if a function 'T tzset()' has been declared. + if constexpr (requires (T (*p)()) { p = &tzset; }) + { + setenv("TZ", "GMT0", 1); + tzset(); // Set C library's global time zone to GMT0 + } +#endif +} + +void +test_c_zoned() +{ +#if _GLIBCXX_USE_CXX11_ABI + maybe_set_tz(); + + using namespace std::chrono; + auto time = sys_days(2025y/March/19) + 15h; + zoned_time z("America/New_York", time); + auto s = std::format(std::locale("aa_DJ.UTF-8"), "{:L%c}", z); + VERIFY( s.find("11:00") != s.npos ); + + // We should not print an incorrect time zone. + VERIFY( s.find("GMT") == s.npos ); + VERIFY( s.find("UTC") == s.npos ); + +#ifdef _GLIBCXX_HAVE_STRUCT_TM_TM_ZONE + // When tm.tm_zone is supported, we do print the correct time zone. + VERIFY( s.find(z.get_info().abbrev) != s.npos ); +#endif +#endif +} + +void +test_c_local() +{ + maybe_set_tz(); + + std::locale("aa_DJ.UTF-8"); + using namespace std::chrono; + auto time = local_days(2025y/March/19) + 11h; + auto s = std::format(std::locale("aa_DJ.UTF-8"), "{:L%c}", time); + // __builtin_printf("%s\n", s.c_str()); return; + VERIFY( s.find("11:00") != s.npos ); + + // We should not print any time zone. + VERIFY( s.find("GMT") == s.npos ); + VERIFY( s.find("UTC") == s.npos ); + +#if _GLIBCXX_USE_CXX11_ABI + zoned_time z(system_clock::now()); + VERIFY( s.find(z.get_info().abbrev) == s.npos ); +#endif +} + int main() { test_c(); + test_c_zoned(); + test_c_local(); }