From 7690aa557494b3d3405088fcfaefcf1f61b41946 Mon Sep 17 00:00:00 2001 From: Victor Zverovich Date: Fri, 6 Nov 2026 09:43:42 -0800 Subject: [PATCH] Simplify float to integral duration_cast and its tests Compute the upper bound as 2^digits with std::ldexp instead of static_cast(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. --- include/fmt/chrono.h | 15 ++++++--------- test/chrono-test.cc | 29 ++++++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 5c19f0c6..d4b44eb3 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -533,16 +533,13 @@ template from) -> To { #if FMT_SAFE_DURATION_CAST using to_rep = typename To::rep; - auto count = detail::duration_cast< - std::chrono::duration>(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(max_value()) + 1; + using float_duration = std::chrono::duration; + auto count = std::trunc(detail::duration_cast(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((std::numeric_limits::min)()); - auto truncated = std::trunc(count); - if (!(truncated >= lower && truncated < upper)) throw_duration_error(); + auto upper = std::ldexp(FromRep(1), std::numeric_limits::digits); + if (!(count >= lower && count < upper)) throw_duration_error(); return To(static_cast(count)); #else return std::chrono::duration_cast(from); diff --git a/test/chrono-test.cc b/test/chrono-test.cc index 4e8ae32e..eac86bec 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -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(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>(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(std::numeric_limits::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; - EXPECT_FALSE(fmt::detail::duration_cast(fsec(0)).count()); EXPECT_TRUE(fmt::detail::duration_cast(fsec(2)).count()); - EXPECT_TRUE(fmt::detail::duration_cast(fsec(-1)).count()); - if (FMT_SAFE_DURATION_CAST) { - EXPECT_THROW( - fmt::detail::duration_cast>(fsec(-1)), - fmt::format_error); - EXPECT_THROW( - fmt::detail::duration_cast>(fsec(min - 1.0)), - fmt::format_error); - } + if (!FMT_SAFE_DURATION_CAST) return; + EXPECT_THROW( + fmt::detail::duration_cast>(fsec(-1)), + fmt::format_error); + EXPECT_THROW( + fmt::detail::duration_cast>(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>( + fsec(9223372036854775808.0)), + fmt::format_error); + EXPECT_THROW(fmt::detail::duration_cast>( + fsec(std::numeric_limits::quiet_NaN())), + fmt::format_error); } struct custom_rep {