-
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 6 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,16 +1389,54 @@ 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( | ||
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)); | ||
} | ||
template <class Duration> class subsecond_helper { | ||
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. Having a utility struct seems unnecessary. I suggest converting it into a function (e.g. 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. Done |
||
/// 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 | ||
matrackif marked this conversation as resolved.
Show resolved
Hide resolved
|
||
static constexpr int num_digits(std::intmax_t num, std::intmax_t den, | ||
int 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 <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) { | ||
return d >= d.zero() ? d : -d; | ||
} | ||
|
||
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) { | ||
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<typename Duration::rep, | ||
std::chrono::seconds::rep>::type, | ||
std::ratio<1, pow10(fractional_width)>>; | ||
|
||
template <class Rep, class Period> | ||
static constexpr typename precision::rep get_subseconds( | ||
std::chrono::duration<Rep, Period> d) { | ||
return std::chrono::treat_as_floating_point<typename precision::rep>::value | ||
? (abs(d) - std::chrono::duration_cast<std::chrono::seconds>(d)) | ||
.count() | ||
: std::chrono::duration_cast<precision>( | ||
abs(d) - | ||
std::chrono::duration_cast<std::chrono::seconds>(d)) | ||
.count(); | ||
} | ||
}; | ||
|
||
template <typename Char, typename Rep, typename OutputIt, | ||
FMT_ENABLE_IF(std::is_integral<Rep>::value)> | ||
|
@@ -1550,11 +1588,11 @@ struct chrono_formatter { | |
} | ||
} | ||
|
||
void write(Rep value, int width) { | ||
template <typename RepType> void write(RepType value, int 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. Why introduce a template parameter instead of using 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.
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 this is correct. 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. Sorry, lowercase |
||
write_sign(); | ||
if (isnan(value)) return write_nan(); | ||
uint32_or_64_or_128_t<int> n = | ||
to_unsigned(to_nonnegative_int(value, max_value<int>())); | ||
uint32_or_64_or_128_t<std::intmax_t> n = | ||
to_unsigned(to_nonnegative_int(value, max_value<std::intmax_t>())); | ||
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 use 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. As you wish, std::chrono::duration types use std::intmax_t internally that's why I went along with it. |
||
int num_digits = detail::count_digits(n); | ||
if (width > num_digits) out = std::fill_n(out, width - num_digits, '0'); | ||
out = format_decimal<char_type>(out, n, num_digits).end; | ||
|
@@ -1637,18 +1675,14 @@ 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)) { | ||
using subsec_helper = detail::subsecond_helper<duration_rep>; | ||
// Could use c++ 17 if constexpr | ||
if (std::ratio_less<typename subsec_helper::precision::period, | ||
std::chrono::seconds::period>::value) { | ||
*out++ = '.'; | ||
write(ms.count(), 3); | ||
write(subsec_helper::get_subseconds(duration_rep{val}), | ||
subsec_helper::fractional_width); | ||
} | ||
return; | ||
} | ||
|
@@ -1678,7 +1712,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 |
---|---|---|
|
@@ -544,14 +544,12 @@ TEST(chrono_test, negative_durations) { | |
|
||
TEST(chrono_test, special_durations) { | ||
EXPECT_EQ( | ||
"40.", | ||
fmt::format("{:%S}", std::chrono::duration<double>(1e20)).substr(0, 3)); | ||
"40", | ||
fmt::format("{:%S}", std::chrono::duration<double>(1e20)).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. Why change this? 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. According to the spec, the decimal point should be printed only if the duration "period" type is more precise than seconds:
This can be determined statically at compile time and does not depend on runtime values of the duration. The test I modified has seconds precision, so I removed the decimal point. In my local instance of Visual Studio 2022, the following test passes:
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. Shall we check that there is no decimal point then? |
||
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 +583,46 @@ TEST(chrono_test, weekday) { | |
} | ||
} | ||
|
||
TEST(chrono_test, cpp20_duration_subsecond_support) { | ||
using attoseconds = std::chrono::duration<std::intmax_t, std::atto>; | ||
// Check that 18 digits of subsecond precision are supported | ||
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: Please add punctuation (. in the end of the sentence) here and elsewhere. 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. Done |
||
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.123456789}), | ||
"-00.123456789"); | ||
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. Why is the integral part 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. According to the spec:
The number of seconds is The MSVC implementation of
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. Oh, I misread the test case and thought that the integral part 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. Done |
||
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 | ||
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. "empty zeros"? I suggest rephrasing this as "zero fractional part" or smth like that. 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. Done |
||
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