Simplify float to integral duration_cast and its tests

Compute the upper bound as 2^digits with std::ldexp instead of
static_cast<FromRep>(max) + 1, which is only exact when rounding to
nearest: with FE_UPWARD in an unoptimized build the bound became
2^63 + 2048 and a count of 2^63 reached the undefined conversion.
Truncate the count once and use it for both the check and the cast.

Skip the checks that need safe duration casts when
FMT_SAFE_DURATION_CAST is 0, where the NaN time point check failed.
Drop the 1e300 time point and two bool assertions, which don't catch
anything the others don't, and test max + 1 and NaN directly.
This commit is contained in:
Victor Zverovich 2026-11-06 09:43:42 -08:00
parent 4ed3535092
commit 7690aa5574
2 changed files with 22 additions and 22 deletions

View File

@ -533,16 +533,13 @@ template <typename To, typename FromRep, typename FromPeriod,
auto duration_cast(std::chrono::duration<FromRep, FromPeriod> from) -> To {
#if FMT_SAFE_DURATION_CAST
using to_rep = typename To::rep;
auto count = detail::duration_cast<
std::chrono::duration<FromRep, typename To::period>>(from)
.count();
// The conversion truncates toward zero so check the truncated value.
// max + 1 is a power of 2 so the cast to FromRep is exact even when max
// itself is rounded up by the conversion; the comparison is false for NaN.
auto upper = static_cast<FromRep>(max_value<to_rep>()) + 1;
using float_duration = std::chrono::duration<FromRep, typename To::period>;
auto count = std::trunc(detail::duration_cast<float_duration>(from).count());
// The conversion truncates, so check the truncated count against
// [min, max + 1) where max + 1 = 2^digits. NaN fails the comparison.
auto lower = static_cast<FromRep>((std::numeric_limits<to_rep>::min)());
auto truncated = std::trunc(count);
if (!(truncated >= lower && truncated < upper)) throw_duration_error();
auto upper = std::ldexp(FromRep(1), std::numeric_limits<to_rep>::digits);
if (!(count >= lower && count < upper)) throw_duration_error();
return To(static_cast<to_rep>(count));
#else
return std::chrono::duration_cast<To>(from);

View File

@ -1053,9 +1053,8 @@ TEST(chrono_test, out_of_range) {
// A floating-point day count that doesn't fit in int.
auto fd = std::chrono::duration<double>(1e300);
EXPECT_THROW((void)fmt::format("{:%j}", fd), fmt::format_error);
// A floating-point time point whose seconds don't fit in time_t.
auto ftp = sys_time<std::chrono::duration<double>>(fd);
EXPECT_THROW((void)fmt::format("{:%T}", ftp), fmt::format_error);
if (!FMT_SAFE_DURATION_CAST) return;
// Floating-point time points whose seconds don't fit in time_t.
auto fnan = sys_time<std::chrono::duration<double>>(
std::chrono::duration<double>(std::numeric_limits<double>::quiet_NaN()));
EXPECT_THROW((void)fmt::format("{:%T}", fnan), fmt::format_error);
@ -1083,17 +1082,21 @@ TEST(chrono_test, float_to_integral_duration_cast) {
max);
// Any nonzero value converts to true.
using bool_duration = std::chrono::duration<bool>;
EXPECT_FALSE(fmt::detail::duration_cast<bool_duration>(fsec(0)).count());
EXPECT_TRUE(fmt::detail::duration_cast<bool_duration>(fsec(2)).count());
EXPECT_TRUE(fmt::detail::duration_cast<bool_duration>(fsec(-1)).count());
if (FMT_SAFE_DURATION_CAST) {
EXPECT_THROW(
fmt::detail::duration_cast<std::chrono::duration<unsigned>>(fsec(-1)),
fmt::format_error);
EXPECT_THROW(
fmt::detail::duration_cast<std::chrono::duration<int>>(fsec(min - 1.0)),
fmt::format_error);
}
if (!FMT_SAFE_DURATION_CAST) return;
EXPECT_THROW(
fmt::detail::duration_cast<std::chrono::duration<unsigned>>(fsec(-1)),
fmt::format_error);
EXPECT_THROW(
fmt::detail::duration_cast<std::chrono::duration<int>>(fsec(min - 1.0)),
fmt::format_error);
// max + 1, which is also what max rounds to in the source type.
EXPECT_THROW(fmt::detail::duration_cast<std::chrono::duration<long long>>(
fsec(9223372036854775808.0)),
fmt::format_error);
EXPECT_THROW(fmt::detail::duration_cast<std::chrono::duration<int>>(
fsec(std::numeric_limits<double>::quiet_NaN())),
fmt::format_error);
}
struct custom_rep {