From f086dc0d275ab2f152d2e81d4398e5a8bcd0d897 Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Sat, 12 Apr 2025 10:14:57 -0700 Subject: [PATCH] Fix timezone handling in tm --- include/fmt/chrono.h | 180 +++++++++++++++++++++---------------------- test/chrono-test.cc | 37 +++++++-- 2 files changed, 115 insertions(+), 102 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index f34db132..b089a7d9 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -420,14 +420,11 @@ auto write(OutputIt out, const std::tm& time, const std::locale& loc, return write_encoded_tm_str(out, string_view(buf.data(), buf.size()), loc); } -template -struct is_same_arithmetic_type - : public std::integral_constant::value && - std::is_integral::value) || - (std::is_floating_point::value && - std::is_floating_point::value)> { -}; +template +using is_similar_arithmetic_type = + bool_constant<(std::is_integral::value && std::is_integral::value) || + (std::is_floating_point::value && + std::is_floating_point::value)>; FMT_NORETURN inline void throw_duration_error() { FMT_THROW(format_error("cannot format duration")); @@ -486,9 +483,9 @@ auto duration_cast(std::chrono::duration from) -> To { #endif } -template < - typename To, typename FromRep, typename FromPeriod, - FMT_ENABLE_IF(!is_same_arithmetic_type::value)> +template ::value)> auto duration_cast(std::chrono::duration from) -> To { // Mixed integer <-> float cast is not supported by safe_duration_cast. return std::chrono::duration_cast(from); @@ -520,6 +517,7 @@ template auto current_zone(T...) -> time_zone* { template void _tzset(T...) {} } // namespace tz +// DEPRECATED! inline void tzset_once() { static bool init = []() { using namespace tz; @@ -915,11 +913,11 @@ template struct null_chrono_spec_handler { class tm_format_checker : public null_chrono_spec_handler { private: - bool no_timezone_ = false; + bool has_timezone_ = false; public: - constexpr explicit tm_format_checker(bool no_timezone = false) - : no_timezone_(no_timezone) {} + constexpr explicit tm_format_checker(bool has_timezone) + : has_timezone_(has_timezone) {} FMT_NORETURN inline void unsupported() { FMT_THROW(format_error("no format")); @@ -959,10 +957,10 @@ class tm_format_checker : public null_chrono_spec_handler { FMT_CONSTEXPR void on_iso_time() {} FMT_CONSTEXPR void on_am_pm() {} FMT_CONSTEXPR void on_utc_offset(numeric_system) { - if (no_timezone_) FMT_THROW(format_error("no timezone")); + if (!has_timezone_) FMT_THROW(format_error("no timezone")); } FMT_CONSTEXPR void on_tz_name() { - if (no_timezone_) FMT_THROW(format_error("no timezone")); + if (!has_timezone_) FMT_THROW(format_error("no timezone")); } }; @@ -1094,7 +1092,7 @@ void write_fractional_seconds(OutputIt& out, Duration d, int precision = -1) { // Format subseconds which are given as a floating point type with an // appropriate number of digits. We cannot pass the Duration here, as we -// explicitly need to pass the Rep value in the chrono_formatter. +// explicitly need to pass the Rep value in the duration_formatter. template void write_floating_seconds(memory_buffer& buf, Duration duration, int num_fractional_digits = -1) { @@ -1128,7 +1126,7 @@ class tm_writer { static constexpr int days_per_week = 7; const std::locale& loc_; - const bool is_classic_; + bool is_classic_; OutputIt out_; const Duration* subsecs_; const std::tm& tm_; @@ -1164,8 +1162,8 @@ class tm_writer { } auto tm_hour12() const noexcept -> int { - const auto h = tm_hour(); - const auto z = h < 12 ? h : h - 12; + auto h = tm_hour(); + auto z = h < 12 ? h : h - 12; return z == 0 ? 12 : z; } @@ -1181,11 +1179,11 @@ class tm_writer { // Algorithm: https://en.wikipedia.org/wiki/ISO_week_date. auto iso_year_weeks(long long curr_year) const noexcept -> int { - const auto prev_year = curr_year - 1; - const auto curr_p = + auto prev_year = curr_year - 1; + auto curr_p = (curr_year + curr_year / 4 - curr_year / 100 + curr_year / 400) % days_per_week; - const auto prev_p = + auto prev_p = (prev_year + prev_year / 4 - prev_year / 100 + prev_year / 400) % days_per_week; return 52 + ((curr_p == 4 || prev_p == 3) ? 1 : 0); @@ -1195,15 +1193,15 @@ class tm_writer { days_per_week; } auto tm_iso_week_year() const noexcept -> long long { - const auto year = tm_year(); - const auto w = iso_week_num(tm_yday(), tm_wday()); + auto year = tm_year(); + auto w = iso_week_num(tm_yday(), tm_wday()); if (w < 1) return year - 1; if (w > iso_year_weeks(year)) return year + 1; return year; } auto tm_iso_week_of_year() const noexcept -> int { - const auto year = tm_year(); - const auto w = iso_week_num(tm_yday(), tm_wday()); + auto year = tm_year(); + auto w = iso_week_num(tm_yday(), tm_wday()); if (w < 1) return iso_year_weeks(year - 1); if (w > iso_year_weeks(year)) return 1; return w; @@ -1240,9 +1238,8 @@ class tm_writer { uint32_or_64_or_128_t n = to_unsigned(year); const int num_digits = count_digits(n); if (negative && pad == pad_type::zero) *out_++ = '-'; - if (width > num_digits) { + if (width > num_digits) out_ = detail::write_padding(out_, pad, width - num_digits); - } if (negative && pad != pad_type::zero) *out_++ = '-'; out_ = format_decimal(out_, n, num_digits); } @@ -1467,11 +1464,10 @@ class tm_writer { void on_day_of_year(pad_type pad) { auto yday = tm_yday() + 1; auto digit1 = yday / 100; - if (digit1 != 0) { + if (digit1 != 0) write1(digit1); - } else { + else out_ = detail::write_padding(out_, pad); - } write2(yday % 100, pad); } @@ -1608,18 +1604,16 @@ template ::value)> inline auto get_milliseconds(std::chrono::duration d) -> std::chrono::duration { - // this may overflow and/or the result may not fit in the - // target type. + // This may overflow and/or the result may not fit in the target type. #if FMT_SAFE_DURATION_CAST - using CommonSecondsType = + using common_seconds_type = typename std::common_type::type; - const auto d_as_common = detail::duration_cast(d); - const auto d_as_whole_seconds = + auto d_as_common = detail::duration_cast(d); + auto d_as_whole_seconds = detail::duration_cast(d_as_common); - // this conversion should be nonproblematic - const auto diff = d_as_common - d_as_whole_seconds; - const auto ms = - detail::duration_cast>(diff); + // This conversion should be nonproblematic. + auto diff = d_as_common - d_as_whole_seconds; + auto ms = detail::duration_cast>(diff); return ms; #else auto s = detail::duration_cast(d); @@ -1693,7 +1687,7 @@ class get_locale { template -struct chrono_formatter { +struct duration_formatter { FormatContext& context; OutputIt out; int precision; @@ -1711,8 +1705,8 @@ struct chrono_formatter { using char_type = typename FormatContext::char_type; using tm_writer_type = tm_writer; - chrono_formatter(FormatContext& ctx, OutputIt o, - std::chrono::duration d) + duration_formatter(FormatContext& ctx, OutputIt o, + std::chrono::duration d) : context(ctx), out(o), val(static_cast(d.count())), @@ -1730,19 +1724,16 @@ struct chrono_formatter { // returns true if nan or inf, writes to out. auto handle_nan_inf() -> bool { - if (isfinite(val)) { - return false; - } + if (isfinite(val)) return false; if (isnan(val)) { write_nan(); return true; } // must be +-inf - if (val > 0) { - write_pinf(); - } else { - write_ninf(); - } + if (val > 0) + std::copy_n("inf", 3, out); + else + std::copy_n("-inf", 4, out); return true; } @@ -1770,10 +1761,9 @@ struct chrono_formatter { } void write_sign() { - if (negative) { - *out++ = '-'; - negative = false; - } + if (!negative) return; + *out++ = '-'; + negative = false; } void write(Rep value, int width, pad_type pad = pad_type::zero) { @@ -1789,8 +1779,6 @@ struct chrono_formatter { } void write_nan() { std::copy_n("nan", 3, out); } - void write_pinf() { std::copy_n("inf", 3, out); } - void write_ninf() { std::copy_n("-inf", 4, out); } template void format_tm(const tm& time, Callback cb, Args... args) { @@ -1872,9 +1860,8 @@ struct chrono_formatter { write_floating_seconds(buf, std::chrono::duration(val), precision); if (negative) *out++ = '-'; - if (buf.size() < 2 || buf[1] == '.') { + if (buf.size() < 2 || buf[1] == '.') out = detail::write_padding(out, pad); - } out = copy(buf.begin(), buf.end(), out); } else { write(second(), 2, pad); @@ -1995,7 +1982,7 @@ class year_month_day { constexpr auto month() const noexcept -> fmt::month { return month_; } constexpr auto day() const noexcept -> fmt::day { return day_; } }; -#endif +#endif // __cpp_lib_chrono >= 201907 template struct formatter : private formatter { @@ -2188,9 +2175,9 @@ struct formatter, Char> { out = detail::format_duration_value(out, d.count(), precision); detail::format_duration_unit(out); } else { - using chrono_formatter = - detail::chrono_formatter; - auto f = chrono_formatter(ctx, out, d); + using duration_formatter = + detail::duration_formatter; + auto f = duration_formatter(ctx, out, d); f.precision = precision; f.localized = localized_; detail::parse_chrono_format(begin, end, f); @@ -2204,31 +2191,12 @@ template struct formatter { private: format_specs specs_; detail::arg_ref width_ref_; - - protected: basic_string_view fmt_ = detail::string_literal(); - template - auto do_format(const std::tm& tm, FormatContext& ctx, - const Duration* subsecs) const -> decltype(ctx.out()) { - auto specs = specs_; - auto buf = basic_memory_buffer(); - auto out = basic_appender(buf); - detail::handle_dynamic_spec(specs.dynamic_width(), specs.width, width_ref_, - ctx); - - auto loc_ref = ctx.locale(); - detail::get_locale loc(static_cast(loc_ref), loc_ref); - auto w = - detail::tm_writer(loc, out, tm, subsecs); - detail::parse_chrono_format(fmt_.begin(), fmt_.end(), w); - return detail::write( - ctx.out(), basic_string_view(buf.data(), buf.size()), specs); - } - - FMT_CONSTEXPR auto do_parse(parse_context& ctx, - bool no_timezone = false) -> const Char* { + protected: + FMT_CONSTEXPR auto do_parse(parse_context& ctx, bool has_timezone) + -> const Char* { auto it = ctx.begin(), end = ctx.end(); if (it == end || *it == '}') return it; @@ -2242,15 +2210,33 @@ template struct formatter { } end = detail::parse_chrono_format(it, end, - detail::tm_format_checker(no_timezone)); + detail::tm_format_checker(has_timezone)); // Replace the default format string only if the new spec is not empty. if (end != it) fmt_ = {it, detail::to_unsigned(end - it)}; return end; } + template + auto do_format(const std::tm& tm, FormatContext& ctx, + const Duration* subsecs) const -> decltype(ctx.out()) { + auto specs = specs_; + auto buf = basic_memory_buffer(); + auto out = basic_appender(buf); + detail::handle_dynamic_spec(specs.dynamic_width(), specs.width, width_ref_, + ctx); + + auto loc_ref = ctx.locale(); + detail::get_locale loc(static_cast(loc_ref), loc_ref); + auto w = detail::tm_writer, Char, Duration>( + loc, out, tm, subsecs); + detail::parse_chrono_format(fmt_.begin(), fmt_.end(), w); + return detail::write( + ctx.out(), basic_string_view(buf.data(), buf.size()), specs); + } + public: FMT_CONSTEXPR auto parse(parse_context& ctx) -> const Char* { - return do_parse(ctx); + return do_parse(ctx, detail::has_member_data_tm_gmtoff::value); } template @@ -2260,8 +2246,13 @@ template struct formatter { } }; +// DEPRECATED! Reversed order of template parameters. template -struct formatter, Char> : formatter { +struct formatter, Char> : private formatter { + FMT_CONSTEXPR auto parse(parse_context& ctx) -> const Char* { + return this->do_parse(ctx, true); + } + template auto format(sys_time val, FormatContext& ctx) const -> decltype(ctx.out()) { @@ -2281,7 +2272,7 @@ struct formatter, Char> : formatter { --tm.tm_sec; else tm = gmtime(val - second); - subsecs += detail::duration_cast(std::chrono::seconds(1)); + subsecs += second; } return formatter::do_format(tm, ctx, &subsecs); } @@ -2299,9 +2290,10 @@ struct formatter, Char> }; template -struct formatter, Char> : formatter { +struct formatter, Char> + : private formatter { FMT_CONSTEXPR auto parse(parse_context& ctx) -> const Char* { - return this->do_parse(ctx, true); + return this->do_parse(ctx, false); } template @@ -2310,9 +2302,9 @@ struct formatter, Char> : formatter { auto time_since_epoch = val.time_since_epoch(); auto seconds_since_epoch = detail::duration_cast(time_since_epoch); - // Use gmtime to prevent time conversion since local_time has an + // Use gmtime to prevent time zone conversion since local_time has an // unspecified time zone. - auto t = gmtime(seconds_since_epoch.count()); + std::tm t = gmtime(seconds_since_epoch.count()); using period = typename Duration::period; if (period::num == 1 && period::den == 1 && !std::is_floating_point::value) { diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 8754587c..5b6323f0 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -16,7 +16,6 @@ using fmt::runtime; using fmt::sys_time; -using fmt::sys_time; using testing::Contains; #if defined(__MINGW32__) && !defined(_UCRT) @@ -336,9 +335,31 @@ TEST(chrono_test, local_time) { fmt::format_error, "no timezone"); } +template ::value)> +bool set_gmtoff(T& time, long offset) { + time.tm_gmtoff = offset; + return true; +} +template ::value)> +bool set_gmtoff(T&, long) { + return false; +} + TEST(chrono_test, tm) { auto time = fmt::gmtime(290088000); test_time(time); + if (set_gmtoff(time, -28800)) { + EXPECT_EQ(fmt::format(fmt::runtime("{:%z}"), time), "-0800"); + EXPECT_EQ(fmt::format(fmt::runtime("{:%Ez}"), time), "-08:00"); + EXPECT_EQ(fmt::format(fmt::runtime("{:%Oz}"), time), "-08:00"); + } else { + EXPECT_THROW_MSG((void)fmt::format(fmt::runtime("{:%z}"), time), + fmt::format_error, "no timezone"); + EXPECT_THROW_MSG((void)fmt::format(fmt::runtime("{:%Z}"), time), + fmt::format_error, "no timezone"); + } } TEST(chrono_test, daylight_savings_time_end) { @@ -740,11 +761,11 @@ TEST(chrono_test, cpp20_duration_subsecond_support) { EXPECT_EQ(fmt::format("{:.6%H:%M:%S}", dur), "01:00:01.234000"); } using nanoseconds_dbl = std::chrono::duration; - EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{-123456789}), "-00.123456789"); - EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{9123456789}), "09.123456789"); + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl(-123456789)), "-00.123456789"); + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl(9123456789)), "09.123456789"); // Verify that only the seconds part is extracted and printed. - EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{99123456789}), "39.123456789"); - EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{99123000000}), "39.123000000"); + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl(99123456789)), "39.123456789"); + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl(99123000000)), "39.123000000"); { // Now the hour is printed, and we also test if negative doubles work. auto dur = nanoseconds_dbl{-99123456789}; @@ -755,7 +776,7 @@ TEST(chrono_test, cpp20_duration_subsecond_support) { } // Check that durations with precision greater than std::chrono::seconds have // fixed precision, and print zeros even if there is no fractional part. - EXPECT_EQ(fmt::format("{:%S}", std::chrono::microseconds{7000000}), + EXPECT_EQ(fmt::format("{:%S}", std::chrono::microseconds(7000000)), "07.000000"); EXPECT_EQ(fmt::format("{:%S}", std::chrono::duration>(1)), @@ -775,9 +796,9 @@ TEST(chrono_test, cpp20_duration_subsecond_support) { "-05:27.68"); // Check that floating point seconds with ratio<1,1> are printed. - EXPECT_EQ(fmt::format("{:%S}", std::chrono::duration{1.5}), + EXPECT_EQ(fmt::format("{:%S}", std::chrono::duration(1.5)), "01.500000"); - EXPECT_EQ(fmt::format("{:%M:%S}", std::chrono::duration{-61.25}), + EXPECT_EQ(fmt::format("{:%M:%S}", std::chrono::duration(-61.25)), "-01:01.250000"); }