diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 1a8d8d04..abfb900e 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -966,13 +966,131 @@ inline void tzset_once() { } #endif -template class tm_writer { +// 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(std::is_unsigned::value || + (value >= 0 && to_unsigned(value) <= to_unsigned(upper)), + "invalid value"); + (void)upper; + return static_cast(value); +} +template ::value)> +inline Int to_nonnegative_int(T value, Int upper) { + if (value < 0 || value > static_cast(upper)) + FMT_THROW(format_error("invalid value")); + return static_cast(value); +} + +template ::is_signed)> +constexpr std::chrono::duration abs( + std::chrono::duration d) { + // 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)> +constexpr std::chrono::duration abs( + std::chrono::duration d) { + return d; +} + +constexpr long long pow10(std::uint32_t n) { + return n == 0 ? 1 : 10 * pow10(n - 1); +} + +// Counts the number of fractional digits in the range [0, 18] according to the +// C++20 spec. If more than 18 fractional digits are required then returns 6 for +// microseconds precision. +template () / 10)> +struct count_fractional_digits { + static constexpr int value = + Num % Den == 0 ? N : count_fractional_digits::value; +}; + +// Base case that doesn't instantiate any more templates +// in order to avoid overflow. +template +struct count_fractional_digits { + static constexpr int value = (Num % Den == 0) ? N : 6; +}; + +// Format subseconds which are given as an integer type with an appropriate +// number of digits. +template +void write_fractional_seconds(OutputIt& out, Duration d) { + FMT_ASSERT(!std::is_floating_point::value, ""); + constexpr auto num_fractional_digits = + count_fractional_digits::value; + + using subsecond_precision = std::chrono::duration< + typename std::common_type::type, + std::ratio<1, detail::pow10(num_fractional_digits)>>; + if (std::ratio_less::value) { + *out++ = '.'; + auto fractional = + detail::abs(d) - std::chrono::duration_cast(d); + auto subseconds = + std::chrono::treat_as_floating_point< + typename subsecond_precision::rep>::value + ? fractional.count() + : std::chrono::duration_cast(fractional) + .count(); + uint32_or_64_or_128_t n = + to_unsigned(to_nonnegative_int(subseconds, max_value())); + int num_digits = detail::count_digits(n); + if (num_fractional_digits > num_digits) + out = std::fill_n(out, num_fractional_digits - num_digits, '0'); + out = format_decimal(out, n, num_digits).end; + } +} + +// Format subseconds which are given as a floating point type with an appropiate +// number of digits. We cannot pass the Duration here, as we explicitly need to +// pass the Rep value in the chrono_formatter. +template +void write_floating_seconds(memory_buffer& buf, Duration duration) { + FMT_ASSERT(std::is_floating_point::value, ""); + auto num_fractional_digits = + count_fractional_digits::value; + // For non-integer values, we ensure at least 6 digits to get microsecond + // precision. + auto val = duration.count(); + if (num_fractional_digits < 6 && + static_cast(std::round(val)) != val) + num_fractional_digits = 6; + + format_to( + std::back_inserter(buf), runtime("{:.{}f}"), + std::fmod(val * + static_cast(Duration::period::num) / + static_cast(Duration::period::den), + static_cast(60)), + num_fractional_digits); +} + +template +class tm_writer { private: static constexpr int days_per_week = 7; const std::locale& loc_; const bool is_classic_; OutputIt out_; + const Duration* subsecs_; const std::tm& tm_; auto tm_sec() const noexcept -> int { @@ -1135,10 +1253,12 @@ template class tm_writer { } public: - tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm) + tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm, + const Duration* subsecs = nullptr) : loc_(loc), is_classic_(loc_ == get_classic_locale()), out_(out), + subsecs_(subsecs), tm_(tm) {} OutputIt out() const { return out_; } @@ -1337,9 +1457,26 @@ template class tm_writer { if (is_classic_ || ns == numeric_system::standard) return write2(tm_min()); format_localized('M', 'O'); } + void on_second(numeric_system ns) { - if (is_classic_ || ns == numeric_system::standard) return write2(tm_sec()); - format_localized('S', 'O'); + if (is_classic_ || ns == numeric_system::standard) { + write2(tm_sec()); + if (subsecs_) { + if (std::is_floating_point::value) { + auto buf = memory_buffer(); + write_floating_seconds(buf, *subsecs_); + if (buf.size() > 1) { + // Remove the leading "0", write something like ".123". + out_ = std::copy(buf.begin() + 1, buf.end(), out_); + } + } else { + write_fractional_seconds(out_, *subsecs_); + } + } + } else { + // Currently no formatting of subseconds when a locale is set. + format_localized('S', 'O'); + } } void on_12_hour_time() { @@ -1402,22 +1539,6 @@ 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) { - FMT_ASSERT(std::is_unsigned::value || - (value >= 0 && to_unsigned(value) <= to_unsigned(upper)), - "invalid value"); - (void)upper; - return static_cast(value); -} -template ::value)> -inline Int to_nonnegative_int(T value, Int upper) { - if (value < 0 || value > static_cast(upper)) - FMT_THROW(format_error("invalid value")); - return static_cast(value); -} - template ::value)> inline T mod(T x, int y) { return x % static_cast(y); @@ -1472,47 +1593,6 @@ inline std::chrono::duration get_milliseconds( #endif } -// Counts the number of fractional digits in the range [0, 18] according to the -// C++20 spec. If more than 18 fractional digits are required then returns 6 for -// microseconds precision. -template () / 10)> -struct count_fractional_digits { - static constexpr int value = - Num % Den == 0 ? N : count_fractional_digits::value; -}; - -// Base case that doesn't instantiate any more templates -// in order to avoid overflow. -template -struct count_fractional_digits { - static constexpr int value = (Num % Den == 0) ? N : 6; -}; - -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) { - // 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)> -constexpr std::chrono::duration abs( - std::chrono::duration d) { - return d; -} - template ::value)> OutputIt format_duration_value(OutputIt out, Rep val, int) { @@ -1673,36 +1753,6 @@ struct chrono_formatter { out = format_decimal(out, n, num_digits).end; } - template void write_fractional_seconds(Duration d) { - FMT_ASSERT(!std::is_floating_point::value, ""); - constexpr auto num_fractional_digits = - count_fractional_digits::value; - - using subsecond_precision = std::chrono::duration< - typename std::common_type::type, - std::ratio<1, detail::pow10(num_fractional_digits)>>; - if (std::ratio_less::value) { - *out++ = '.'; - auto fractional = - detail::abs(d) - std::chrono::duration_cast(d); - auto subseconds = - std::chrono::treat_as_floating_point< - typename subsecond_precision::rep>::value - ? fractional.count() - : std::chrono::duration_cast(fractional) - .count(); - uint32_or_64_or_128_t n = - to_unsigned(to_nonnegative_int(subseconds, max_value())); - int num_digits = detail::count_digits(n); - if (num_fractional_digits > num_digits) - out = std::fill_n(out, num_fractional_digits - 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); } @@ -1780,20 +1830,15 @@ struct chrono_formatter { if (ns == numeric_system::standard) { if (std::is_floating_point::value) { - constexpr auto num_fractional_digits = - count_fractional_digits::value; auto buf = memory_buffer(); - format_to(std::back_inserter(buf), runtime("{:.{}f}"), - std::fmod(val * static_cast(Period::num) / - static_cast(Period::den), - static_cast(60)), - num_fractional_digits); + write_floating_seconds(buf, std::chrono::duration(val)); if (negative) *out++ = '-'; if (buf.size() < 2 || buf[1] == '.') *out++ = '0'; out = std::copy(buf.begin(), buf.end(), out); } else { write(second(), 2); - write_fractional_seconds(std::chrono::duration(val)); + write_fractional_seconds( + out, std::chrono::duration(val)); } return; } @@ -2016,28 +2061,41 @@ struct formatter, this->do_parse(default_specs.begin(), default_specs.end()); } - template - auto format(std::chrono::time_point val, + template + auto format(std::chrono::time_point> + val, FormatContext& ctx) const -> decltype(ctx.out()) { - return formatter::format(localtime(val), ctx); + if (Period::num != 1 || Period::den != 1 || + std::is_floating_point::value) { + const auto epoch = val.time_since_epoch(); + const auto subsecs = + std::chrono::duration_cast>( + epoch - std::chrono::duration_cast(epoch)); + + return formatter::format( + localtime(std::chrono::time_point_cast(val)), + ctx, subsecs); + } + + return formatter::format( + localtime(std::chrono::time_point_cast(val)), + ctx); } }; #if FMT_USE_UTC_TIME template struct formatter, - Char> : formatter { - FMT_CONSTEXPR formatter() { - basic_string_view default_specs = - detail::string_literal{}; - this->do_parse(default_specs.begin(), default_specs.end()); - } - - template - auto format(std::chrono::time_point val, + Char> + : formatter, + Char> { + template + auto format(std::chrono::time_point val, FormatContext& ctx) const -> decltype(ctx.out()) { - return formatter::format( - localtime(std::chrono::utc_clock::to_sys(val)), ctx); + return formatter< + std::chrono::time_point, + Char>::format(std::chrono::utc_clock::to_sys(val), ctx); } }; #endif @@ -2089,6 +2147,22 @@ template struct formatter { detail::parse_chrono_format(specs.begin(), specs.end(), w); return w.out(); } + + template + auto format(const std::tm& tm, FormatContext& ctx, + const Duration& subsecs) const -> decltype(ctx.out()) { + const auto loc_ref = ctx.locale(); + detail::get_locale loc(static_cast(loc_ref), loc_ref); + auto w = detail::tm_writer( + loc, ctx.out(), tm, &subsecs); + if (spec_ == spec::year_month_day) + w.on_iso_date(); + else if (spec_ == spec::hh_mm_ss) + w.on_iso_time(); + else + detail::parse_chrono_format(specs.begin(), specs.end(), w); + return w.out(); + } }; FMT_MODULE_EXPORT_END diff --git a/test/chrono-test.cc b/test/chrono-test.cc index bc474a40..fbc9fd61 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -253,7 +253,8 @@ template auto strftime_full(TimePoint tp) -> std::string { } TEST(chrono_test, time_point) { - auto t1 = std::chrono::system_clock::now(); + auto t1 = std::chrono::time_point_cast( + std::chrono::system_clock::now()); EXPECT_EQ(strftime_full(t1), fmt::format("{:%Y-%m-%d %H:%M:%S}", t1)); EXPECT_EQ(strftime_full(t1), fmt::format("{}", t1)); using time_point = @@ -634,10 +635,18 @@ TEST(chrono_test, cpp20_duration_subsecond_support) { // fixed precision, and print zeros even if there is no fractional part. EXPECT_EQ(fmt::format("{:%S}", std::chrono::microseconds{7000000}), "07.000000"); - EXPECT_EQ(fmt::format("{:%S}", std::chrono::duration>(1)), + EXPECT_EQ(fmt::format("{:%S}", + std::chrono::duration>(1)), "00.333333"); - EXPECT_EQ(fmt::format("{:%S}", std::chrono::duration>(1)), + EXPECT_EQ(fmt::format("{:%S}", + std::chrono::duration>(1)), "00.142857"); + + // Check that floating point seconds with ratio<1,1> are printed. + EXPECT_EQ(fmt::format("{:%S}", std::chrono::duration{1.5}), + "01.500000"); + EXPECT_EQ(fmt::format("{:%M:%S}", std::chrono::duration{-61.25}), + "-01:01.250000"); } #endif // FMT_STATIC_THOUSANDS_SEPARATOR @@ -652,3 +661,66 @@ TEST(chrono_test, utc_clock) { fmt::format("{:%Y-%m-%d %H:%M:%S}", t1_utc)); } #endif + +TEST(chrono_test, timestamps_sub_seconds) { + std::chrono::time_point>> + t1(std::chrono::duration>(4)); + + EXPECT_EQ(fmt::format("{:%S}", t1), "01.333333"); + + std::chrono::time_point>> + t2(std::chrono::duration>(4)); + + EXPECT_EQ(fmt::format("{:%S}", t2), "01.333333"); + + const std::chrono::time_point + t3(std::chrono::seconds(2)); + + EXPECT_EQ(fmt::format("{:%S}", t3), "02"); + + const std::chrono::time_point> + t4(std::chrono::duration>(9.5)); + + EXPECT_EQ(fmt::format("{:%S}", t4), "09.500000"); + + const std::chrono::time_point> + t5(std::chrono::duration>(9)); + + EXPECT_EQ(fmt::format("{:%S}", t5), "09"); + + const std::chrono::time_point + t6(std::chrono::seconds(1) + std::chrono::milliseconds(120)); + + EXPECT_EQ(fmt::format("{:%S}", t6), "01.120"); + + const std::chrono::time_point + t7(std::chrono::microseconds(1234567)); + + EXPECT_EQ(fmt::format("{:%S}", t7), "01.234567"); + + const std::chrono::time_point + t8(std::chrono::nanoseconds(123456789)); + + EXPECT_EQ(fmt::format("{:%S}", t8), "00.123456789"); + + const auto t9 = std::chrono::time_point_cast( + std::chrono::system_clock::now()); + const auto t9_sec = std::chrono::time_point_cast(t9); + auto t9_sub_sec_part = fmt::format("{0:09}", (t9 - t9_sec).count()); + + EXPECT_EQ(fmt::format("{}.{}", strftime_full(t9_sec), t9_sub_sec_part), + fmt::format("{:%Y-%m-%d %H:%M:%S}", t9)); + + const std::chrono::time_point + t10(std::chrono::milliseconds(2000)); + + EXPECT_EQ(fmt::format("{:%S}", t10), "02.000"); +} diff --git a/test/xchar-test.cc b/test/xchar-test.cc index dd45826d..608af350 100644 --- a/test/xchar-test.cc +++ b/test/xchar-test.cc @@ -285,7 +285,8 @@ std::wstring system_wcsftime(const std::wstring& format, const std::tm* timeptr, } TEST(chrono_test_wchar, time_point) { - auto t1 = std::chrono::system_clock::now(); + auto t1 = std::chrono::time_point_cast( + std::chrono::system_clock::now()); std::vector spec_list = { L"%%", L"%n", L"%t", L"%Y", L"%EY", L"%y", L"%Oy", L"%Ey", L"%C",