diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index d54c8ae7..60e5a459 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -1320,19 +1320,19 @@ inline bool isfinite(T) { // Converts value to int and checks that it's in the range [0, upper). template ::value)> -inline int to_nonnegative_int(T value, int upper) { +inline std::intmax_t to_nonnegative_int(T value, std::intmax_t upper) { FMT_ASSERT(value >= 0 && to_unsigned(value) <= to_unsigned(upper), "invalid value"); (void)upper; - return static_cast(value); + return static_cast(value); } template ::value)> -inline int to_nonnegative_int(T value, int upper) { +inline std::intmax_t to_nonnegative_int(T value, std::intmax_t upper) { FMT_ASSERT( std::isnan(value) || (value >= 0 && value <= static_cast(upper)), "invalid value"); (void)upper; - return static_cast(value); + return static_cast(value); } template ::value)> @@ -1389,16 +1389,55 @@ inline std::chrono::duration get_milliseconds( #endif } -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)); -} +template class subsecond_helper { + /// Returns the amount of digits according to the c++ 20 spec + /// In the range [0, 18], if more than 18 fractional digits are required, + /// then we return 6 for microseconds precision + static constexpr int num_digits(std::intmax_t num, + std::intmax_t den, + std::uint32_t n = 0) { + return num % den == 0 ? n : (n > 18 ? 6 : num_digits(num * 10, den, n + 1)); + } + + static constexpr std::intmax_t pow10(std::uint32_t n) { + return n == 0 ? 1 : 10 * pow10(n - 1); + } + + template ::is_signed)> + static constexpr std::chrono::duration abs( + std::chrono::duration d) { + return d >= d.zero() ? d : -d; + } + + template ::is_signed)> + static constexpr std::chrono::duration abs( + std::chrono::duration d) { + return d; + } + + public: + static constexpr auto fractional_width = + num_digits(Duration::period::num, Duration::period::den); + + using precision = std::chrono::duration< + typename std::common_type::type, + std::ratio<1, pow10(fractional_width)>>; + + template + static constexpr typename precision::rep get_subseconds( + std::chrono::duration d) { + return std::chrono::treat_as_floating_point::value + ? (abs(d) - std::chrono::duration_cast(d)) + .count() + : std::chrono::duration_cast( + abs(d) - + std::chrono::duration_cast(d)) + .count(); + } +}; template ::value)> @@ -1553,8 +1592,8 @@ struct chrono_formatter { void write(Rep value, int width) { write_sign(); if (isnan(value)) return write_nan(); - uint32_or_64_or_128_t n = - to_unsigned(to_nonnegative_int(value, max_value())); + uint32_or_64_or_128_t n = + to_unsigned(to_nonnegative_int(value, max_value())); int num_digits = detail::count_digits(n); if (width > num_digits) out = std::fill_n(out, width - num_digits, '0'); out = format_decimal(out, n, num_digits).end; @@ -1645,10 +1684,12 @@ struct chrono_formatter { #else auto tmpval = std::chrono::duration(val); #endif - auto ms = get_milliseconds(tmpval); - if (ms != std::chrono::milliseconds(0)) { + using subsec_helper = detail::subsecond_helper; + // Could use c++ 17 if constexpr + if (std::ratio_less::value) { *out++ = '.'; - write(ms.count(), 3); + write(subsec_helper::get_subseconds(tmpval), subsec_helper::fractional_width); } return; } @@ -1678,7 +1719,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 a2bc20c2..85673683 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -544,8 +544,8 @@ TEST(chrono_test, negative_durations) { TEST(chrono_test, special_durations) { EXPECT_EQ( - "40.", - fmt::format("{:%S}", std::chrono::duration(1e20)).substr(0, 3)); + "40", + fmt::format("{:%S}", std::chrono::duration(1e20)).substr(0, 2)); auto nan = std::numeric_limits::quiet_NaN(); EXPECT_EQ( "nan nan nan nan nan:nan nan", @@ -585,4 +585,44 @@ 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{673231113420148734}), + "00.673231113420148734"); + EXPECT_EQ(fmt::format("{:%S}", attoseconds{-673231113420148734}), + "-00.673231113420148734"); + EXPECT_EQ(fmt::format("{:%S}", std::chrono::nanoseconds{13420148734}), + "13.420148734"); + EXPECT_EQ(fmt::format("{:%S}", std::chrono::nanoseconds{-13420148734}), + "-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{3601234}; + 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"); + 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}; + 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 have + // fixed precision and empty zeros + EXPECT_EQ(fmt::format("{:%S}", std::chrono::microseconds{7000000}), + "07.000000"); +} + #endif // FMT_STATIC_THOUSANDS_SEPARATOR