diff --git a/include/boost/decimal/decimal128_t.hpp b/include/boost/decimal/decimal128_t.hpp index 41c7bfcfd..7847cbd92 100644 --- a/include/boost/decimal/decimal128_t.hpp +++ b/include/boost/decimal/decimal128_t.hpp @@ -745,7 +745,7 @@ constexpr decimal128_t::decimal128_t(T1 coeff, T2 exp, bool sign) noexcept auto biased_exp {static_cast(exp + detail::bias_v)}; BOOST_DECIMAL_IF_CONSTEXPR (sizeof(T1) >= sizeof(significand_type)) { - if (coeff > detail::d128_max_significand_value || biased_exp < 0) + if (coeff > detail::d128_max_significand_value || biased_exp < -(detail::precision_v - 1)) { coeff_digits = detail::coefficient_rounding(coeff, exp, biased_exp, sign, detail::num_digits(coeff)); } @@ -788,6 +788,19 @@ constexpr decimal128_t::decimal128_t(T1 coeff, T2 exp, bool sign) noexcept reduced_coeff *= detail::pow10(static_cast(digit_delta)); *this = decimal128_t(reduced_coeff, exp, sign); } + else if (coeff_digits + biased_exp <= detail::precision_v) + { + // Handle the case of sub-normals that don't need further rounding + bits_.high = sign ? detail::d128_sign_mask : UINT64_C(0); // Reset the sign bit + const auto zeros {detail::remove_trailing_zeros(reduced_coeff)}; + biased_exp += static_cast(zeros.number_of_removed_zeros); + reduced_coeff = zeros.trimmed_number; + if (biased_exp > 0) + { + reduced_coeff *= detail::pow10(static_cast(biased_exp)); + } + bits_ |= reduced_coeff; + } else if (digit_delta < 0 && coeff_digits - digit_delta <= detail::precision_v) { const auto offset {detail::precision_v - coeff_digits}; diff --git a/include/boost/decimal/decimal32_t.hpp b/include/boost/decimal/decimal32_t.hpp index 18963f799..3ca3f8ca0 100644 --- a/include/boost/decimal/decimal32_t.hpp +++ b/include/boost/decimal/decimal32_t.hpp @@ -653,7 +653,7 @@ constexpr decimal32_t::decimal32_t(T1 coeff, T2 exp, bool sign) noexcept // NOLI // Only count the number of digits if we absolutely have to int coeff_digits {-1}; auto biased_exp {static_cast(exp + detail::bias)}; - if (coeff > detail::d32_max_significand_value || biased_exp < 0) + if (coeff > detail::d32_max_significand_value || biased_exp < -(detail::precision_v - 1)) { coeff_digits = detail::coefficient_rounding(coeff, exp, biased_exp, sign, detail::num_digits(coeff)); } @@ -710,6 +710,19 @@ constexpr decimal32_t::decimal32_t(T1 coeff, T2 exp, bool sign) noexcept // NOLI reduced_coeff *= detail::pow10(static_cast(digit_delta)); *this = decimal32_t(reduced_coeff, exp, sign); } + else if (coeff_digits + biased_exp <= detail::precision) + { + // Handle the case of sub-normals that don't need further rounding + bits_ = sign ? detail::d32_sign_mask : UINT32_C(0); // Reset the sign bit + const auto zeros {detail::remove_trailing_zeros(reduced_coeff)}; + biased_exp += static_cast(zeros.number_of_removed_zeros); + reduced_coeff = zeros.trimmed_number; + if (biased_exp > 0) + { + reduced_coeff *= detail::pow10(static_cast(biased_exp)); + } + bits_ |= reduced_coeff; + } else if (digit_delta < 0 && coeff_digits - digit_delta <= detail::precision) { // We can expand the coefficient to use the maximum number of digits diff --git a/include/boost/decimal/decimal64_t.hpp b/include/boost/decimal/decimal64_t.hpp index 7391c5730..12919617b 100644 --- a/include/boost/decimal/decimal64_t.hpp +++ b/include/boost/decimal/decimal64_t.hpp @@ -647,7 +647,7 @@ constexpr decimal64_t::decimal64_t(T1 coeff, T2 exp, bool sign) noexcept // If the coeff is not in range, make it so int coeff_digits {-1}; auto biased_exp {static_cast(exp) + detail::bias_v}; - if (coeff > detail::d64_max_significand_value || biased_exp < 0) + if (coeff > detail::d64_max_significand_value || biased_exp < -(detail::precision_v - 1)) { coeff_digits = detail::coefficient_rounding(coeff, exp, biased_exp, sign, detail::num_digits(coeff)); } @@ -704,6 +704,19 @@ constexpr decimal64_t::decimal64_t(T1 coeff, T2 exp, bool sign) noexcept reduced_coeff *= detail::pow10(static_cast(digit_delta)); *this = decimal64_t(reduced_coeff, exp, sign); } + else if (coeff_digits + biased_exp <= detail::precision_v) + { + // Handle the case of sub-normals that don't need further rounding + bits_ = sign ? detail::d64_sign_mask : UINT64_C(0); // Reset the sign bit + const auto zeros {detail::remove_trailing_zeros(reduced_coeff)}; + biased_exp += static_cast(zeros.number_of_removed_zeros); + reduced_coeff = zeros.trimmed_number; + if (biased_exp > 0) + { + reduced_coeff *= detail::pow10(static_cast(biased_exp)); + } + bits_ |= reduced_coeff; + } else if (digit_delta < 0 && coeff_digits - digit_delta <= detail::precision_v) { const auto offset {detail::precision_v - coeff_digits}; diff --git a/include/boost/decimal/detail/cmath/next.hpp b/include/boost/decimal/detail/cmath/next.hpp index ada64a1eb..fbd15c8bd 100644 --- a/include/boost/decimal/detail/cmath/next.hpp +++ b/include/boost/decimal/detail/cmath/next.hpp @@ -53,9 +53,24 @@ constexpr auto nextafter_impl(const DecimalType val, const bool direction) noexc if (!isnormal(val)) { - // Not to make sure that denorms aren't normalized + // Denorms need separate handling sig = removed_zeros.trimmed_number; exp += static_cast(removed_zeros.number_of_removed_zeros); + + if (removed_zeros.number_of_removed_zeros > 0) + { + // We need to shift an add + // 1 -> 11 instead of 2 since 11e-101 < 2e-100 starting at 1e-100 + sig *= 10U; + ++sig; + --exp; + } + else + { + ++sig; + } + + return DecimalType{sig, exp, is_neg}; } if (direction) diff --git a/test/github_issue_1105.cpp b/test/github_issue_1105.cpp index 5de27526b..2baf85f64 100644 --- a/test/github_issue_1105.cpp +++ b/test/github_issue_1105.cpp @@ -45,9 +45,29 @@ void test() const auto zero_next {nextafter(zero, one)}; BOOST_TEST_EQ(zero_next, std::numeric_limits::denorm_min()); +} + +// Per IEEE 754 nextafter is allowed to break cohort +void test_non_preserving() +{ + const auto val = decimal32_t{"1e-100"}; + const auto two_val = decimal32_t{2, boost::decimal::detail::etiny_v}; + const auto one = decimal32_t{"1e0"}; + const auto next = nextafter(val,one); + const auto between = decimal32_t{"11e-101"}; - const auto two_next_zero {nextafter(zero_next, one)}; - BOOST_TEST_EQ(two_next_zero, T(2, detail::etiny_v)); + BOOST_TEST_LE(val, between); + BOOST_TEST_EQ(next, between); + BOOST_TEST_LE(two_val, next); + + const auto nines_value = decimal32_t{"99e-101"}; + const auto next_nines_res = decimal32_t{"991e-102"}; + const auto res = nextafter(nines_value, one); + BOOST_TEST_EQ(res, next_nines_res); + + const auto wrap_value = decimal32_t{"9999999e-107"}; + const auto next_after_wrap = nextafter(wrap_value, one); + BOOST_TEST_GT(next_after_wrap, wrap_value); } int main() @@ -56,5 +76,7 @@ int main() test(); test(); + test_non_preserving(); + return boost::report_errors(); }