diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index a1c13483..220e4648 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -2233,27 +2233,25 @@ struct formatter, Char> auto format(local_time val, FormatContext& ctx) const -> decltype(ctx.out()) { auto time_since_epoch = val.time_since_epoch(); - auto seconds_since_epoch = - detail::duration_cast(time_since_epoch); + auto secs = detail::duration_cast(time_since_epoch); + // duration_cast truncates towards zero, so the remainder is negative + // before the epoch. Compute it before flooring the seconds because + // converting floored seconds back overflows at the limits, e.g. + // nanoseconds(INT64_MIN). Keep it in the common duration type as well + // since Duration may be too narrow, e.g. duration. + auto subsecs = time_since_epoch - secs; + if (subsecs.count() < 0) { + secs -= std::chrono::seconds(1); + subsecs += std::chrono::seconds(1); + } // Use gmtime to prevent time zone conversion since local_time has an // unspecified time zone. - std::tm t = gmtime(seconds_since_epoch.count()); + std::tm t = gmtime(secs.count()); using period = typename Duration::period; if (period::num == 1 && period::den == 1 && !std::is_floating_point::value) { return formatter::format(t, ctx); } - auto subsecs = - detail::duration_cast(time_since_epoch - seconds_since_epoch); - if (subsecs.count() < 0) { - auto second = detail::duration_cast(std::chrono::seconds(1)); - if (t.tm_sec != 0) { - --t.tm_sec; - } else { - t = gmtime(seconds_since_epoch.count() - 1); - } - subsecs += second; - } return formatter::do_format(t, ctx, &subsecs); } }; diff --git a/test/chrono-test.cc b/test/chrono-test.cc index d9ea18a1..ece11752 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -342,6 +342,44 @@ TEST(chrono_test, local_time) { EXPECT_EQ(fmt::format("{:%S}", pre_epoch), "01.123"); } +TEST(chrono_test, local_time_before_epoch) { + using std::chrono::milliseconds; + using std::chrono::nanoseconds; + using std::chrono::seconds; + auto ms = [](long long n) { + return fmt::local_time(milliseconds(n)); + }; + // Subseconds are a remainder of a floored second, not a negative offset. + EXPECT_EQ(fmt::format("{}", ms(-500)), "1969-12-31 23:59:59.500"); + EXPECT_EQ(fmt::format("{}", ms(-1000)), "1969-12-31 23:59:59.000"); + EXPECT_EQ(fmt::format("{}", ms(-1500)), "1969-12-31 23:59:58.500"); + // Flooring the second also moves to the previous minute. + EXPECT_EQ(fmt::format("{}", ms(-61500)), "1969-12-31 23:58:58.500"); + EXPECT_EQ(fmt::format("{}", fmt::local_time>( + std::chrono::duration(-1.5))), + "1969-12-31 23:59:58.500000"); + // A whole-second duration has no fractional part to format. + EXPECT_EQ(fmt::format("{}", fmt::local_time(seconds(-1))), + "1969-12-31 23:59:59"); + // Flooring the seconds must not convert them back to the input precision, + // which would overflow. Only a sanitizer detects this, so the result is + // irrelevant and gmtime may reject the value on platforms with a narrower + // supported range. + auto ns_min = nanoseconds((std::numeric_limits::min)()); + try { + (void)fmt::format("{}", fmt::local_time(ns_min)); + } catch (const fmt::format_error&) { + } +} + +TEST(chrono_test, local_time_narrow_duration) { + // int16_t microseconds hold less than a second, so the subsecond remainder + // does not fit in the duration itself. + using narrow = std::chrono::duration; + EXPECT_EQ(fmt::format("{}", fmt::local_time(narrow(-30000))), + "1969-12-31 23:59:59.970000"); +} + template ::value)> auto set_tm_gmtoff(T& time, long offset) -> bool { time.tm_gmtoff = offset;