diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index 40a465a2..f805911c 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -524,11 +524,13 @@ auto duration_cast(std::chrono::duration from) -> To { 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; auto lower = static_cast((std::numeric_limits::min)()); - if (!(count >= lower && count < upper)) throw_duration_error(); + auto truncated = std::trunc(count); + if (!(truncated >= lower && truncated < 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 fe9b4fa8..4f7eabd0 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -1006,6 +1006,33 @@ TEST(chrono_test, out_of_range) { EXPECT_THROW((void)fmt::format("{:%F}", flocal), fmt::format_error); } +TEST(chrono_test, float_to_integral_duration_cast) { + using fsec = std::chrono::duration; + // Values that truncate toward zero into range are valid. + EXPECT_EQ( + fmt::detail::duration_cast>(fsec(-0.5)) + .count(), + 0u); + auto min = (std::numeric_limits::min)(); + EXPECT_EQ( + fmt::detail::duration_cast>(fsec(min - 0.5)) + .count(), + min); + auto max = (std::numeric_limits::max)(); + EXPECT_EQ( + fmt::detail::duration_cast>(fsec(max + 0.5)) + .count(), + max); + 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); + } +} + TEST(chrono_test, calendar_padding) { EXPECT_EQ("05 ", fmt::format("{:5}", fmt::day(5))); EXPECT_EQ(" Jan", fmt::format("{:>5}", fmt::month(1)));