diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 5e8098d6..e38f6dcf 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -886,39 +886,57 @@ To fmt_safe_duration_cast(std::chrono::duration from) { } #endif -template ::value)> -inline std::chrono::duration get_milliseconds( - std::chrono::duration d) { - // this may overflow and/or the result may not fit in the - // target type. -#if FMT_SAFE_DURATION_CAST - using CommonSecondsType = - typename std::common_type::type; - const auto d_as_common = fmt_safe_duration_cast(d); - const auto d_as_whole_seconds = - fmt_safe_duration_cast(d_as_common); - // this conversion should be nonproblematic - const auto diff = d_as_common - d_as_whole_seconds; - const auto ms = - fmt_safe_duration_cast>(diff); - return ms; -#else - auto s = std::chrono::duration_cast(d); - return std::chrono::duration_cast(d - s); -#endif -} +template struct subsecond_helper { + static constexpr unsigned int fractional_width = [] { + static_assert(Duration::period::num > 0 && Duration::period::den > 0, + "Numerator and denominator can't be less than 1."); + auto num = Duration::period::num; + constexpr auto den = Duration::period::den; + // Returns the number of fractional digits of num / den in the range + // [0, 18]. If it can't be represented, 6 is returned. Example: + // fractional_width(1, 8) would return 3 for 0.125. + unsigned int result = 0; + for (; num % den != 0 && result < 19; num = num % den * 10, ++result) { + } + return result == 19 ? 6 : result; + }(); -template ::value)> -inline std::chrono::duration get_milliseconds( - std::chrono::duration d) { - using common_type = typename std::common_type::type; - auto ms = mod(d.count() * static_cast(Period::num) / - static_cast(Period::den) * 1000, - 1000); - return std::chrono::duration(static_cast(ms)); -} + static constexpr intmax_t pow10(const unsigned int exp) { + intmax_t result = 1; + for (unsigned int i = 0; i < exp; ++i) { + result *= 10; + } + return result; + } + + template + static constexpr std::chrono::duration abs( + std::chrono::duration d) { + if FMT_CONSTEXPR (std::chrono::duration::min() < + std::chrono::duration::zero()) { + return d >= d.zero() ? d : -d; + } else { + return d; + } + } + + using precision = std::chrono::duration< + typename std::common_type::type, + std::ratio<1, pow10(fractional_width)>>; + + template + static constexpr precision get_subseconds( + std::chrono::duration d) { + if FMT_CONSTEXPR (std::chrono::treat_as_floating_point_v< + typename precision::rep>) { + return abs(d) - std::chrono::duration_cast(d); + } else { + return std::chrono::duration_cast( + abs(d) - std::chrono::duration_cast(d)); + } + } +}; template ::value)> @@ -1138,14 +1156,22 @@ struct chrono_formatter { // convert rep->Rep using duration_rep = std::chrono::duration; using duration_Rep = std::chrono::duration; - auto tmpval = fmt_safe_duration_cast(duration_rep{val}); + const auto tmpval = + fmt_safe_duration_cast(duration_rep{val}); #else - auto tmpval = std::chrono::duration(val); + const auto tmpval = std::chrono::duration(val); #endif - auto ms = get_milliseconds(tmpval); - if (ms != std::chrono::milliseconds(0)) { + using subsec_helper = detail::subsecond_helper; + const std::uintmax_t subseconds = + subsec_helper::get_subseconds(tmpval).count(); + if (subseconds > 0) { *out++ = '.'; - write(ms.count(), 3); + const auto num_digits = detail::count_digits(subseconds); + if (subsec_helper::fractional_width > num_digits) { + out = std::fill_n(out, subsec_helper::fractional_width - num_digits, + '0'); + } + out = format_decimal(out, subseconds, num_digits).end; } return; } @@ -1175,7 +1201,7 @@ struct chrono_formatter { on_24_hour_time(); *out++ = ':'; if (handle_nan_inf()) return; - write(second(), 2); + on_second(numeric_system::standard); } void on_am_pm() { diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 648ba600..4e19a1d5 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -500,7 +500,7 @@ TEST(chrono_test, negative_durations) { TEST(chrono_test, special_durations) { EXPECT_EQ( - "40.", + "40", fmt::format("{:%S}", std::chrono::duration(1e20)).substr(0, 3)); auto nan = std::numeric_limits::quiet_NaN(); EXPECT_EQ( @@ -533,4 +533,41 @@ TEST(chrono_test, weekday) { } } +TEST(chrono_test, cpp20_duration_subsecond_support) { + using attoseconds = std::chrono::duration; + // Check that 18 digits of subsecond precision are supported + EXPECT_EQ(fmt::format("{:%S}", attoseconds{673'231'113'420'148'734}), + "00.673231113420148734"); + EXPECT_EQ(fmt::format("{:%S}", attoseconds{-673'231'113'420'148'734}), + "-00.673231113420148734"); + EXPECT_EQ(fmt::format("{:%S}", std::chrono::nanoseconds{13'420'148'734}), + "13.420148734"); + EXPECT_EQ(fmt::format("{:%S}", std::chrono::nanoseconds{-13'420'148'734}), + "-13.420148734"); + EXPECT_EQ(fmt::format("{:%S}", std::chrono::milliseconds{1234}), "01.234"); + { + // Check that {:%H:%M:%S} is equivalent to {:%T} + auto dur = std::chrono::milliseconds{3'601'234}; + auto formatted_dur = fmt::format("{:%T}", dur); + EXPECT_EQ(formatted_dur, "01:00:01.234"); + EXPECT_EQ(fmt::format("{:%H:%M:%S}", dur), formatted_dur); + } + using nanoseconds_dbl = std::chrono::duration; + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{-123456789.123456789}), + "-00.123456789"); + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{9123456789.123456789}), + "09.123456789"); + // Only seconds part is printed + EXPECT_EQ(fmt::format("{:%S}", nanoseconds_dbl{99123456789}), "39.123456789"); + { + // Now the hour is printed, and we also test if negative doubles work + auto dur = nanoseconds_dbl{-99123456789}; + auto formatted_dur = fmt::format("{:%T}", dur); + EXPECT_EQ(formatted_dur, "-00:01:39.123456789"); + EXPECT_EQ(fmt::format("{:%H:%M:%S}", dur), formatted_dur); + } + // Check that durations with precision greater than std::chrono::seconds + // with no subsecond part print as regular seconds + EXPECT_EQ(fmt::format("{:%S}", std::chrono::microseconds{7'000'000}), "07"); +} #endif // FMT_STATIC_THOUSANDS_SEPARATOR