diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index d277328f..b71195d0 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -1388,21 +1388,21 @@ inline bool isfinite(T) { return true; } -// 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) { +// 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) { 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) { +template ::value)> +inline Int to_nonnegative_int(T value, Int 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)> @@ -1459,15 +1459,35 @@ inline std::chrono::duration get_milliseconds( #endif } -template ::value)> -inline std::chrono::duration get_milliseconds( +// Returns the number 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. +constexpr int num_digits(long long num, long long den, int n = 0) { + return num % den == 0 ? n : (n > 18 ? 6 : num_digits(num * 10, den, n + 1)); +} + +constexpr long long pow10(std::uint32_t n) { + return n == 0 ? 1 : 10 * pow10(n - 1); +} + +template ::is_signed)> +constexpr std::chrono::duration abs( 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)); + // We need to compare the duration using the count() method directly + // due to a compiler bug in clang-11 regarding the spaceship operator, + // when -Wzero-as-null-pointer-constant is enabled. + // In clang-12 the bug has been fixed. See + // https://bugs.llvm.org/show_bug.cgi?id=46235 and the reproducible example: + // https://www.godbolt.org/z/Knbb5joYx + return d.count() >= d.zero().count() ? d : -d; +} + +template ::is_signed)> +static constexpr std::chrono::duration abs( + std::chrono::duration d) { + return d; } template (out, n, num_digits).end; } + template void write_fractional_seconds(Duration d) { + constexpr auto fractional_width = + detail::num_digits(Duration::period::num, Duration::period::den); + + using subsecond_precision = std::chrono::duration< + typename std::common_type::type, + std::ratio<1, detail::pow10(fractional_width)>>; + // We could use c++ 17 if constexpr here. + if (std::ratio_less::value) { + *out++ = '.'; + const auto subseconds = + std::chrono::treat_as_floating_point< + typename subsecond_precision::rep>::value + ? (detail::abs(d) - + std::chrono::duration_cast(d)) + .count() + : std::chrono::duration_cast( + detail::abs(d) - + std::chrono::duration_cast(d)) + .count(); + uint32_or_64_or_128_t n = + to_unsigned(to_nonnegative_int(subseconds, max_value())); + int num_digits = detail::count_digits(n); + if (fractional_width > num_digits) { + out = std::fill_n(out, fractional_width - num_digits, '0'); + } + out = format_decimal(out, n, num_digits).end; + } + } + 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); } @@ -1707,19 +1759,7 @@ struct chrono_formatter { if (ns == numeric_system::standard) { write(second(), 2); -#if FMT_SAFE_DURATION_CAST - // convert rep->Rep - using duration_rep = std::chrono::duration; - using duration_Rep = std::chrono::duration; - auto tmpval = fmt_safe_duration_cast(duration_rep{val}); -#else - auto tmpval = std::chrono::duration(val); -#endif - auto ms = get_milliseconds(tmpval); - if (ms != std::chrono::milliseconds(0)) { - *out++ = '.'; - write(ms.count(), 3); - } + write_fractional_seconds(std::chrono::duration{val}); return; } auto time = tm(); @@ -1748,7 +1788,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..42360e5f 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -543,15 +543,12 @@ TEST(chrono_test, negative_durations) { } TEST(chrono_test, special_durations) { - EXPECT_EQ( - "40.", - fmt::format("{:%S}", std::chrono::duration(1e20)).substr(0, 3)); + auto value = fmt::format("{:%S}", std::chrono::duration(1e20)); + EXPECT_EQ(value, "40"); auto nan = std::numeric_limits::quiet_NaN(); EXPECT_EQ( "nan nan nan nan nan:nan nan", fmt::format("{:%I %H %M %S %R %r}", std::chrono::duration(nan))); - (void)fmt::format("{:%S}", - std::chrono::duration(1.79400457e+31f)); EXPECT_EQ(fmt::format("{}", std::chrono::duration(1)), "1Es"); EXPECT_EQ(fmt::format("{}", std::chrono::duration(1)), @@ -585,4 +582,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{999999999999999999}), + "00.999999999999999999"); + 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}), "-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"); + { + // 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 print zeros even if there is no fractional part. + EXPECT_EQ(fmt::format("{:%S}", std::chrono::microseconds{7000000}), + "07.000000"); +} + #endif // FMT_STATIC_THOUSANDS_SEPARATOR