-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement c++20 std::chrono::duration subsecond formatting #2623
Changes from 11 commits
c379573
57d35cc
863d2ff
beacfd3
b8e3e33
5619e87
7544467
3d860b7
0fcdb01
55f62cd
a0abbed
38fb7a6
89c8198
9344c47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1319,20 +1319,20 @@ inline bool isfinite(T) { | |
} | ||
|
||
// Converts value to int and checks that it's in the range [0, upper). | ||
template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)> | ||
inline int to_nonnegative_int(T value, int upper) { | ||
template <typename T, typename Int, FMT_ENABLE_IF(std::is_integral<T>::value)> | ||
vitaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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<int>(value); | ||
return static_cast<Int>(value); | ||
} | ||
template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)> | ||
inline int to_nonnegative_int(T value, int upper) { | ||
template <typename T, typename Int, FMT_ENABLE_IF(!std::is_integral<T>::value)> | ||
inline Int to_nonnegative_int(T value, Int upper) { | ||
FMT_ASSERT( | ||
std::isnan(value) || (value >= 0 && value <= static_cast<T>(upper)), | ||
"invalid value"); | ||
(void)upper; | ||
return static_cast<int>(value); | ||
return static_cast<Int>(value); | ||
} | ||
|
||
template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)> | ||
|
@@ -1389,15 +1389,35 @@ inline std::chrono::duration<Rep, std::milli> get_milliseconds( | |
#endif | ||
} | ||
|
||
template <typename Rep, typename Period, | ||
FMT_ENABLE_IF(std::is_floating_point<Rep>::value)> | ||
inline std::chrono::duration<Rep, std::milli> get_milliseconds( | ||
// Returns the amount of digits according to the c++ 20 spec | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: amount -> number There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok |
||
// In the range [0, 18], if more than 18 fractional digits are required, | ||
// then we return 6 for microseconds precision. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is returning 6 actually required by the standard? Can we turn it into a compile-time error instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the standard:
Microseconds precision is 6 digits. The MSVC implementation of |
||
static constexpr int num_digits(long long num, long long den, int n = 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think static is needed here (and similarly in a functions below). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, static made sense when it was a member function, now it is no longer is needed. |
||
return num % den == 0 ? n : (n > 18 ? 6 : num_digits(num * 10, den, n + 1)); | ||
} | ||
|
||
static constexpr long long pow10(std::uint32_t n) { | ||
return n == 0 ? 1 : 10 * pow10(n - 1); | ||
} | ||
|
||
template <class Rep, class Period, | ||
FMT_ENABLE_IF(std::numeric_limits<Rep>::is_signed)> | ||
static constexpr std::chrono::duration<Rep, Period> abs( | ||
std::chrono::duration<Rep, Period> d) { | ||
using common_type = typename std::common_type<Rep, std::intmax_t>::type; | ||
auto ms = mod(d.count() * static_cast<common_type>(Period::num) / | ||
static_cast<common_type>(Period::den) * 1000, | ||
1000); | ||
return std::chrono::duration<Rep, std::milli>(static_cast<Rep>(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 <class Rep, class Period, | ||
FMT_ENABLE_IF(!std::numeric_limits<Rep>::is_signed)> | ||
static constexpr std::chrono::duration<Rep, Period> abs( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more static to remove here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed in a follow-up commit. |
||
std::chrono::duration<Rep, Period> d) { | ||
return d; | ||
} | ||
|
||
template <typename Char, typename Rep, typename OutputIt, | ||
|
@@ -1560,6 +1580,38 @@ struct chrono_formatter { | |
out = format_decimal<char_type>(out, n, num_digits).end; | ||
} | ||
|
||
template <class Duration> void write_fractional_seconds(Duration d) { | ||
static constexpr auto fractional_width = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't need to be static because it's just an int. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, lightweight integral types probably do not need to be static, even if |
||
detail::num_digits(Duration::period::num, Duration::period::den); | ||
|
||
using subsecond_precision = std::chrono::duration< | ||
typename std::common_type<typename Duration::rep, | ||
std::chrono::seconds::rep>::type, | ||
std::ratio<1, detail::pow10(fractional_width)>>; | ||
// We could use c++ 17 if constexpr here. | ||
if (std::ratio_less<typename subsecond_precision::period, | ||
std::chrono::seconds::period>::value) { | ||
*out++ = '.'; | ||
const auto subseconds = | ||
std::chrono::treat_as_floating_point< | ||
typename subsecond_precision::rep>::value | ||
? (detail::abs(d) - | ||
std::chrono::duration_cast<std::chrono::seconds>(d)) | ||
.count() | ||
: std::chrono::duration_cast<subsecond_precision>( | ||
detail::abs(d) - | ||
std::chrono::duration_cast<std::chrono::seconds>(d)) | ||
.count(); | ||
uint32_or_64_or_128_t<long long> n = | ||
to_unsigned(to_nonnegative_int(subseconds, max_value<long long>())); | ||
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<char_type>(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); } | ||
|
@@ -1637,19 +1689,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<rep, Period>; | ||
using duration_Rep = std::chrono::duration<Rep, Period>; | ||
auto tmpval = fmt_safe_duration_cast<duration_Rep>(duration_rep{val}); | ||
#else | ||
auto tmpval = std::chrono::duration<Rep, Period>(val); | ||
#endif | ||
auto ms = get_milliseconds(tmpval); | ||
if (ms != std::chrono::milliseconds(0)) { | ||
*out++ = '.'; | ||
write(ms.count(), 3); | ||
} | ||
write_fractional_seconds(std::chrono::duration<rep, Period>{val}); | ||
return; | ||
} | ||
auto time = tm(); | ||
|
@@ -1678,7 +1718,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() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -543,15 +543,14 @@ TEST(chrono_test, negative_durations) { | |
} | ||
|
||
TEST(chrono_test, special_durations) { | ||
EXPECT_EQ( | ||
"40.", | ||
fmt::format("{:%S}", std::chrono::duration<double>(1e20)).substr(0, 3)); | ||
auto value = fmt::format("{:%S}", std::chrono::duration<double>(1e20)); | ||
// No decimal point is printed so size() is 2. | ||
EXPECT_EQ(value.size(), 2); | ||
EXPECT_EQ("40", value.substr(0, 2)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's simplify this: EXPECT_EQ(value, "40"); Then we don't need a comment because it's clear that we don't emit a decimal point. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course. I recall the size of this string being platform dependent but after the changes in this PR it should always be consistent. Done |
||
auto nan = std::numeric_limits<double>::quiet_NaN(); | ||
EXPECT_EQ( | ||
"nan nan nan nan nan:nan nan", | ||
fmt::format("{:%I %H %M %S %R %r}", std::chrono::duration<double>(nan))); | ||
(void)fmt::format("{:%S}", | ||
std::chrono::duration<float, std::atto>(1.79400457e+31f)); | ||
EXPECT_EQ(fmt::format("{}", std::chrono::duration<float, std::exa>(1)), | ||
"1Es"); | ||
EXPECT_EQ(fmt::format("{}", std::chrono::duration<float, std::atto>(1)), | ||
|
@@ -585,4 +584,44 @@ TEST(chrono_test, weekday) { | |
} | ||
} | ||
|
||
TEST(chrono_test, cpp20_duration_subsecond_support) { | ||
using attoseconds = std::chrono::duration<long long, std::atto>; | ||
// 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}), | ||
vitaut marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"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<double, std::nano>; | ||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
int -> Int
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done