From 34b1d055f74ec8e7b7dd175a790029cf34500d0e Mon Sep 17 00:00:00 2001 From: Shen-Ta Hsieh Date: Sun, 13 Sep 2026 14:03:01 +0800 Subject: [PATCH] Round a quotient of a non-default mode by its remainder - The divides of the non-default rounding modes dropped the remainder of the wide quotient before the constructor rounded it. A mode which must move away from the truncated quotient could not. - The 32 and 64 bit types held five or six digits past the precision, thus the defect showed only when all of them were zero. The 128 bit types held zero or one digit, thus most quotients were wrong, and fe_dec_to_nearest_from_zero was wrong too. - The remainder now goes in as one sticky digit after the wide quotient. The 128 bit divide also takes one digit more, thus the constructor has a quotient digit to round before the sticky digit. - Both operands of the 128 bit divide have 34 digits, thus the quotient has at most 37 and the block for a wider quotient was dead. It went away. Fixes #1453 --- include/boost/decimal/detail/div_impl.hpp | 40 ++++++----- test/Jamfile | 1 + test/github_issue_1453.cpp | 88 +++++++++++++++++++++++ 3 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 test/github_issue_1453.cpp diff --git a/include/boost/decimal/detail/div_impl.hpp b/include/boost/decimal/detail/div_impl.hpp index 046210c5b..0ef288c56 100644 --- a/include/boost/decimal/detail/div_impl.hpp +++ b/include/boost/decimal/detail/div_impl.hpp @@ -260,9 +260,12 @@ BOOST_DECIMAL_FORCE_INLINE BOOST_DECIMAL_CUDA_CONSTEXPR auto generic_div_impl(co constexpr auto wide_offset {std::numeric_limits::digits10 - precision_v}; constexpr auto wide_tens {pow10(static_cast(wide_offset))}; const auto wide_sig {static_cast(lhs_c.sig) * wide_tens}; - const auto wide_q {wide_sig / static_cast(rhs_c.sig)}; - const auto wide_exp {(lhs_c.exp - static_cast(wide_offset)) - rhs_c.exp}; - return DecimalType{wide_q, wide_exp, sign}; + const auto wide_div {static_cast(rhs_c.sig)}; + const auto wide_q {wide_sig / wide_div}; + // The constructor rounds by the digits it drops, thus the remainder goes in as one digit + const auto sticky {static_cast(wide_sig - wide_q * wide_div != 0U)}; + const auto wide_exp {(lhs_c.exp - static_cast(wide_offset) - 1) - rhs_c.exp}; + return DecimalType{wide_q * 10U + sticky, wide_exp, sign}; } constexpr auto ten_to_p {pow10(static_cast(precision_v))}; @@ -301,9 +304,12 @@ BOOST_DECIMAL_FORCE_INLINE BOOST_DECIMAL_CUDA_CONSTEXPR auto d64_generic_div_imp constexpr auto wide_offset {std::numeric_limits::digits10 - precision_v}; const auto wide_tens {pow10(static_cast(wide_offset))}; const auto wide_sig {static_cast(lhs_c.sig) * wide_tens}; - const auto wide_q {wide_sig / static_cast(rhs_c.sig)}; - const auto wide_exp {(lhs_c.exp - static_cast(wide_offset)) - rhs_c.exp}; - return DecimalType{wide_q, wide_exp, sign}; + const auto wide_div {static_cast(rhs_c.sig)}; + const auto wide_q {wide_sig / wide_div}; + // The constructor rounds by the digits it drops, thus the remainder goes in as one digit + const auto sticky {static_cast(wide_sig - wide_q * wide_div != 0U)}; + const auto wide_exp {(lhs_c.exp - static_cast(wide_offset) - 1) - rhs_c.exp}; + return DecimalType{wide_q * 10U + sticky, wide_exp, sign}; } constexpr auto ten_to_p {static_cast(pow10(static_cast(precision_v)))}; @@ -340,21 +346,17 @@ BOOST_DECIMAL_CUDA_CONSTEXPR auto d128_generic_div_impl(const T& lhs, const T& r if (BOOST_DECIMAL_UNLIKELY(!impl::div_default_rounding(lhs_c.sig))) { - const auto wide_tens {pow10(int128::uint128_t{static_cast(precision_v)})}; + // One digit past the precision for the constructor to round, then the remainder goes in + // as one more digit. Both operands have 34 digits, thus the quotient has at most 37. + const auto wide_tens {pow10(int128::uint128_t{static_cast(precision_v + 1)})}; const auto wide_sig {detail::umul256(lhs_c.sig, wide_tens)}; - auto wide_q {wide_sig / rhs_c.sig}; - auto wide_exp {lhs_c.exp - rhs_c.exp - static_cast(precision_v)}; + const auto wide_dr {impl::div_mod(wide_sig, rhs_c.sig)}; + const auto wide_exp {lhs_c.exp - rhs_c.exp - static_cast(precision_v) - 2}; - if (wide_q[3] != 0U || wide_q[2] != 0U) - { - const auto sig_dig {detail::num_digits(wide_q)}; - const auto digit_delta {sig_dig - std::numeric_limits::digits10}; - wide_q /= pow10(int128::uint128_t{static_cast(digit_delta)}); - wide_exp += digit_delta; - } - - BOOST_DECIMAL_ASSERT((wide_q[3] | wide_q[2]) == 0U); - return DecimalType{int128::uint128_t{wide_q[1], wide_q[0]}, wide_exp, sign}; + BOOST_DECIMAL_ASSERT((wide_dr.quotient[3] | wide_dr.quotient[2]) == 0U); + const int128::uint128_t wide_q {wide_dr.quotient[1], wide_dr.quotient[0]}; + const auto sticky {static_cast(wide_dr.remainder != u256{})}; + return DecimalType{wide_q * 10U + sticky, wide_exp, sign}; } constexpr auto ten_to_p {pow10(int128::uint128_t{static_cast(precision_v)})}; diff --git a/test/Jamfile b/test/Jamfile index 3aeab29fe..819ce7e6a 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -103,6 +103,7 @@ run github_issue_1437.cpp ; run github_issue_1440.cpp ; run github_issue_1447.cpp ; run github_issue_1451.cpp ; +run github_issue_1453.cpp ; run link_1.cpp link_2.cpp link_3.cpp ; run quick.cpp ; diff --git a/test/github_issue_1453.cpp b/test/github_issue_1453.cpp new file mode 100644 index 000000000..13643b766 --- /dev/null +++ b/test/github_issue_1453.cpp @@ -0,0 +1,88 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// https://github.com/boostorg/decimal/issues/1453 +// +// The divide dropped the remainder of its wide quotient before the constructor rounded it, +// thus a directed mode could not move away from the truncated quotient. + +#include +#include + +using namespace boost::decimal; +using namespace boost::decimal::literals; + +// fesetround changes the mode only when the library can find a constant evaluation. +#ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION + +template +void check(const rounding_mode mode, const T lhs, const T rhs, const T expected) +{ + fesetround(mode); + BOOST_TEST_EQ(lhs / rhs, expected); + BOOST_TEST_EQ(-lhs / -rhs, expected); +} + +// below and above are the neighbours of the exact quotient, and nearest is the one of the +// two which is nearer to it. An exact quotient is its own neighbour in every mode. +template +void test_quotients(const T lhs, const T rhs, const T below, const T above, const T nearest) +{ + check(rounding_mode::fe_dec_upward, lhs, rhs, above); + check(rounding_mode::fe_dec_upward, -lhs, rhs, -below); + + check(rounding_mode::fe_dec_downward, lhs, rhs, below); + check(rounding_mode::fe_dec_downward, -lhs, rhs, -above); + + check(rounding_mode::fe_dec_toward_zero, lhs, rhs, below); + check(rounding_mode::fe_dec_toward_zero, -lhs, rhs, -below); + + check(rounding_mode::fe_dec_to_nearest_from_zero, lhs, rhs, nearest); + check(rounding_mode::fe_dec_to_nearest_from_zero, -lhs, rhs, -nearest); +} + +#endif + +int main() +{ + #ifndef BOOST_DECIMAL_NO_CONSTEVAL_DETECTION + + // The last pair of each type has a quotient whose digits past the precision are zero for + // more digits than the wide quotient holds, thus only the remainder can select the direction. + test_quotients(1_DF, 4_DF, 2.5e-01_DF, 2.5e-01_DF, 2.5e-01_DF); + test_quotients(1_DF, 3_DF, 3.333333e-01_DF, 3.333334e-01_DF, 3.333333e-01_DF); + test_quotients(2_DF, 3_DF, 6.666666e-01_DF, 6.666667e-01_DF, 6.666667e-01_DF); + test_quotients(1_DF, 1.000001_DF, 9.99999e-01_DF, 9.999991e-01_DF, 9.99999e-01_DF); + + test_quotients(1_DFF, 4_DFF, 2.5e-01_DFF, 2.5e-01_DFF, 2.5e-01_DFF); + test_quotients(1_DFF, 3_DFF, 3.333333e-01_DFF, 3.333334e-01_DFF, 3.333333e-01_DFF); + test_quotients(2_DFF, 3_DFF, 6.666666e-01_DFF, 6.666667e-01_DFF, 6.666667e-01_DFF); + test_quotients(1_DFF, 1.000001_DFF, 9.99999e-01_DFF, 9.999991e-01_DFF, 9.99999e-01_DFF); + + test_quotients(1_DD, 4_DD, 2.5e-01_DD, 2.5e-01_DD, 2.5e-01_DD); + test_quotients(1_DD, 3_DD, 3.333333333333333e-01_DD, 3.333333333333334e-01_DD, 3.333333333333333e-01_DD); + test_quotients(2_DD, 3_DD, 6.666666666666666e-01_DD, 6.666666666666667e-01_DD, 6.666666666666667e-01_DD); + test_quotients(1_DD, 1.000000000000001_DD, 9.99999999999999e-01_DD, 9.999999999999991e-01_DD, 9.99999999999999e-01_DD); + + test_quotients(1_DDF, 4_DDF, 2.5e-01_DDF, 2.5e-01_DDF, 2.5e-01_DDF); + test_quotients(1_DDF, 3_DDF, 3.333333333333333e-01_DDF, 3.333333333333334e-01_DDF, 3.333333333333333e-01_DDF); + test_quotients(2_DDF, 3_DDF, 6.666666666666666e-01_DDF, 6.666666666666667e-01_DDF, 6.666666666666667e-01_DDF); + test_quotients(1_DDF, 1.000000000000001_DDF, 9.99999999999999e-01_DDF, 9.999999999999991e-01_DDF, 9.99999999999999e-01_DDF); + + test_quotients(1_DL, 4_DL, 2.5e-01_DL, 2.5e-01_DL, 2.5e-01_DL); + test_quotients(1_DL, 3_DL, 3.333333333333333333333333333333333e-01_DL, 3.333333333333333333333333333333334e-01_DL, 3.333333333333333333333333333333333e-01_DL); + test_quotients(2_DL, 3_DL, 6.666666666666666666666666666666666e-01_DL, 6.666666666666666666666666666666667e-01_DL, 6.666666666666666666666666666666667e-01_DL); + test_quotients(1_DL, 1.000000000000000000000000000000001_DL, 9.99999999999999999999999999999999e-01_DL, 9.999999999999999999999999999999991e-01_DL, 9.99999999999999999999999999999999e-01_DL); + + test_quotients(1_DLF, 4_DLF, 2.5e-01_DLF, 2.5e-01_DLF, 2.5e-01_DLF); + test_quotients(1_DLF, 3_DLF, 3.333333333333333333333333333333333e-01_DLF, 3.333333333333333333333333333333334e-01_DLF, 3.333333333333333333333333333333333e-01_DLF); + test_quotients(2_DLF, 3_DLF, 6.666666666666666666666666666666666e-01_DLF, 6.666666666666666666666666666666667e-01_DLF, 6.666666666666666666666666666666667e-01_DLF); + test_quotients(1_DLF, 1.000000000000000000000000000000001_DLF, 9.99999999999999999999999999999999e-01_DLF, 9.999999999999999999999999999999991e-01_DLF, 9.99999999999999999999999999999999e-01_DLF); + + fesetround(rounding_mode::fe_dec_to_nearest); + + #endif + + return boost::report_errors(); +}