diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index ada902de8b2..0f95c5f8704 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -334,6 +334,8 @@ class Number final static constexpr internalrep kMaxRep = std::numeric_limits::max(); static_assert(kMaxRep == 9'223'372'036'854'775'807); static_assert(-kMaxRep == std::numeric_limits::min() + 1); + static constexpr internalrep kMaxRepUp = ((kMaxRep / 10) + 1) * 10; + static_assert(kMaxRepUp == 9'223'372'036'854'775'810ULL); // May need to make unchecked private struct Unchecked diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp index a694e604862..9222c2cb28c 100644 --- a/src/libxrpl/basics/Number.cpp +++ b/src/libxrpl/basics/Number.cpp @@ -287,6 +287,25 @@ class Number::Guard void doDropDigit(T& mantissa, int& exponent) noexcept; + // Modify the result to the correctly rounded value + template + void + doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); + + // Modify the result to the correctly rounded value + template + void + doRoundDown(bool& negative, T& mantissa, int& exponent) const; + + // Modify the result to the correctly rounded value + void + doRound(rep& drops, std::string location) const; + +private: + template + void + pushOverflow(T mantissa); + enum class Round { // The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or // higher. @@ -299,37 +318,22 @@ class Number::Guard // The result was exactly half-way between two integers. This will round to even. Even = 0, // Round up. Always adds 1 (or subtracts 1 in some cases if cuspRoundingFix is not - // Enabled) + // Enabled330) Up = 1, }; - // Indicate round direction: 1 is up, -1 is down, 0 is even + // Indicate round direction. See Round enum above. // This enables the client to round towards nearest, and on // tie, round towards even. [[nodiscard]] Round round() const noexcept; - // Modify the result to the correctly rounded value - template - void - doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location); - - // Modify the result to the correctly rounded value - template - void - doRoundDown(bool& negative, T& mantissa, int& exponent); - - // Modify the result to the correctly rounded value - void - doRound(rep& drops, std::string location) const; - -private: void doPush(unsigned d) noexcept; template void - bringIntoRange(bool& negative, T& mantissa, int& exponent); + bringIntoRange(bool& negative, T& mantissa, int& exponent) const; }; inline void @@ -406,10 +410,76 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce ++exponent; } +template +void +Number::Guard::pushOverflow(T mantissa) +{ + XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::pushOverflow : valid mantissa"); + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa >= kMaxRep && + mantissa < kMaxRepUp) + { + // Special case rounding rules for the values in the range [kMaxRep, kMaxRepUp). + + auto constexpr spread = kMaxRepUp - kMaxRep; + static_assert(spread == 3); + + // Round in two steps. + + // The first step uses the digits _already_ in the Guard to possibly round the mantissa up. + // Ultimately, the purpose of this step is to capture rounding where the stored digits would + // change the decision without those digits. (e.g. From just _below_ the midpoint to just + // _above_ the midpoint for ToNearest, or from kMaxRep into the in-between for Upward. Make + // an exception if the final digit is 9, because it can only get larger, and we don't want + // to bump up to kMaxRepUp. + if (mantissa % 10 < 9) + { + // Intentionally use integer math to get the largest value under the midpoint. + auto constexpr kMidpoint = kMaxRep + (spread / 2); + static_assert(kMidpoint == kMaxRep + 1); + auto const r = round(); + if (r == Round::Up || (r == Round::Even && mantissa == kMidpoint)) + { + ++mantissa; + } + } + + if (mantissa == kMaxRep) + { + // If the mantissa ends up exactly kMaxRep, there's nothing more to do here. + return; + } + + // The second step scales the final digit of the update mantissa proportionally, converting + // from (kMaxRep, kMaxRepUp) to (0 to 9]. It then pushes that scaled digit onto the guard as + // if it was a digit that got removed, but doesn't actually remove it. This method should be + // future-proof in case the number of mantissa bits ever changes. (Though for integer values + // of the form 2^(2^x-1), the spread will always be the same.) Effects: + // * For round to nearest + // * if the updated mantissa is below the midpoint, it'll round "down" to kMaxRep + // * if above the midpoint, it'll round "up" to kMaxRepUp + // * it can never be exactly at the midpoint, because kMaxRepUp is always even, and + // kMaxRep is always odd, so don't worry about that case. + // * For round upward, will round up to kMaxRepUp for positive values, down to kMaxRep for + // negative. + // * For round downward, does the opposite of upward. + // * For round toward zero, always rounds down to kMaxRep. + + auto const diff = mantissa - kMaxRep; + auto digit = (diff * 10) / spread; + XRPL_ASSERT( + digit > 0 && digit < 10 && digit != 5, + "xrpl::Number::Guard::pushOverflow : valid overflow digit"); + + // Don't remove the digit from the mantissa, but add it to the guard as if it was. + push(digit); + } +} + // Returns: -// -1 if Guard is less than half -// 0 if Guard is exactly half -// 1 if Guard is greater than half +// Exact if Guard is _zero_, and appropriate amendments are enabled +// Down if Guard is less than half +// Even if Guard is exactly half +// Up if Guard is greater than half Number::Guard::Round Number::Guard::round() const noexcept { @@ -455,16 +525,19 @@ Number::Guard::round() const noexcept template void -Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) +Number::Guard::bringIntoRange(bool& negative, T& mantissa, int& exponent) const { // Bring mantissa back into the minMantissa / maxMantissa range AFTER - // rounding + // rounding. Mantissa should never be 0. + XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa"); if (mantissa < minMantissa) { mantissa *= 10; --exponent; } - if (exponent < kMinExponent) + // mantissa should never be 0, but if it _is_ make the result kZero. + if (exponent < kMinExponent || + (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0)) { static constexpr Number kZero = Number{}; @@ -478,7 +551,9 @@ template void Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location) { - auto r = round(); + pushOverflow(mantissa); + + auto const r = round(); if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1)) { auto const safeToIncrement = [this](auto const& mantissa) { @@ -495,18 +570,29 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } else { - // Incrementing the mantissa will require dividing, which will require rounding. So - // _don't_ increment the mantissa. Instead, divide and round recursively. It should - // be impossible to recurse more than once, because once the mantissa is divided by - // 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 will have no - // chance of bringing it back over. - doDropDigit(mantissa, exponent); - XRPL_ASSERT_PARTS( - safeToIncrement(mantissa), - "xrpl::Number::Guard::doRoundUp", - "can't recurse more than once"); - doRoundUp(negative, mantissa, exponent, location); - return; + if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && + mantissa > kMaxRep && mantissa < kMaxRepUp) + { + // When rounding up a value in between kMaxRep, and kMaxRepUp, round to + // kMaxRepUp. Note that the decision for this rounding is dominated by the + // results of pushOverflow. + mantissa = kMaxRepUp; + } + else + { + // Incrementing the mantissa will require dividing, which will require rounding. + // So _don't_ increment the mantissa. Instead, divide and round recursively. It + // should be impossible to recurse more than once, because once the mantissa is + // divided by 10, it will be _well_ under maxMantissa and kMaxRep, so adding 1 + // will have no chance of bringing it back over. + doDropDigit(mantissa, exponent); + XRPL_ASSERT_PARTS( + safeToIncrement(mantissa), + "xrpl::Number::Guard::doRoundUp", + "can't recurse more than once"); + doRoundUp(negative, mantissa, exponent, location); + return; + } } } else @@ -524,6 +610,14 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string } } } + else if ( + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa > kMaxRep && + mantissa < kMaxRepUp) + { + // When rounding down a value in between kMaxRep, and kMaxRepUp, round to kMaxRep. + // Note that the decision for this rounding is dominated by the results of pushOverflow. + mantissa = kMaxRep; + } bringIntoRange(negative, mantissa, exponent); if (exponent > kMaxExponent) Throw(std::string(location)); @@ -531,8 +625,10 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string template void -Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) +Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) const { + // Do not pushOverflow here. + auto r = round(); if (cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330) { @@ -567,6 +663,8 @@ Number::Guard::doRoundDown(bool& negative, T& mantissa, int& exponent) void Number::Guard::doRound(rep& drops, std::string location) const { + // Do not pushOverflow here. + auto r = round(); if (r == Round::Up || (r == Round::Even && (drops & 1) == 1)) { @@ -583,6 +681,8 @@ Number::Guard::doRound(rep& drops, std::string location) const } ++drops; } + XRPL_ASSERT(drops >= 0, "xrpl::Number::Guard::doRound : positive magnitude"); + if (isNegative()) drops = -drops; } @@ -632,7 +732,9 @@ doNormalize( { static constexpr auto kMinExponent = Number::kMinExponent; static constexpr auto kMaxExponent = Number::kMaxExponent; - static constexpr auto kMaxRep = Number::kMaxRep; + auto const repLimit = cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 + ? Number::kMaxRepUp + : Number::kMaxRep; using Guard = Number::Guard; @@ -682,17 +784,17 @@ doNormalize( // 9,900,000,000,000,123,450 or 9,900,000,000,000,123,460. // mantissa() will return mantissa / 10, and exponent() will return // exponent + 1. - if (m > kMaxRep) + if (m > repLimit) { if (exponent >= kMaxExponent) throw std::overflow_error("Number::normalize 1.5"); g.doDropDigit(m, exponent); } // Before modification, m should be within the min/max range. After - // modification, it must be less than kMaxRep. In other words, the original - // value should have been no more than kMaxRep * 10. - // (kMaxRep * 10 > maxMantissa) - XRPL_ASSERT_PARTS(m <= kMaxRep, "xrpl::doNormalize", "intermediate mantissa fits in int64"); + // modification, it must be less than repLimit. In other words, the original + // value should have been no more than repLimit * 10. + // (repLimit * 10 > maxMantissa) + XRPL_ASSERT_PARTS(m <= repLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit"); mantissa = m; g.doRoundUp(negative, mantissa, exponent, "Number::normalize 2"); @@ -824,6 +926,9 @@ Number::operator+=(Number const& y) auto const& maxMantissa = g.maxMantissa; auto const cuspRoundingFix = g.cuspRoundingFix; + auto const repLimit = + cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; + // Bring the exponents of both values into agreement, so the mantissas are on the same scale // and can be added directly together. @@ -908,7 +1013,7 @@ Number::operator+=(Number const& y) } else { - if (xm > maxMantissa || xm > kMaxRep) + if (xm > maxMantissa || xm > repLimit) { g.doDropDigit(xm, xe); } @@ -952,7 +1057,7 @@ Number::operator+=(Number const& y) { // Grow xm/xe and pull digits out of the Guard until it's back in the // minMantissa/maxMantissa range. - while (xm < minMantissa && xm * 10 <= kMaxRep) + while (xm < minMantissa && xm * 10 <= repLimit) { xm *= 10; xm -= g.pop(); @@ -1026,8 +1131,10 @@ Number::operator*=(Number const& y) g.setNegative(); auto const& maxMantissa = g.maxMantissa; + auto const repLimit = + g.cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 ? kMaxRepUp : kMaxRep; - while (zm > maxMantissa || zm > kMaxRep) + while (zm > maxMantissa || zm > repLimit) { g.doDropDigit(zm, ze); } diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 0edb955b902..5f318560c11 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -1452,18 +1452,54 @@ class LoanBroker_test : public beast::unit_test::Suite env(tx2, Ter(temINVALID)); } + if (Number::getMantissaScale() == MantissaRange::MantissaScale::Large330) { - auto const dm = power(2, 63) - 1; - BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); - tx2[sfDebtMaximum] = dm; - env(tx2, Ter(temINVALID)); - } + // For the Large330 scale, 2^63 rounds _down_ to Number::kMaxRep + { + auto const dm = power(2, 63); + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm < kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } + { + auto const dm = power(2, 63) + 3; + BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(temINVALID)); + } + } + else { - auto const dm = power(2, 63) - 3; - BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); - tx2[sfDebtMaximum] = dm; - env(tx2, Ter(tesSUCCESS)); + // For other scales, 2^63 rounds _up_ to Number::kMaxRepUp. Subtracting 1 rounds up + // again. + { + auto const dm = power(2, 63) - 1; + BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(temINVALID)); + } + + { + auto const dm = power(2, 63) - 3; + BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm)); + tx2[sfDebtMaximum] = dm; + env(tx2, Ter(tesSUCCESS)); + } } { diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 2c83ad91ecc..a0d3fde035d 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -5809,9 +5809,9 @@ class Vault_test : public beast::unit_test::Suite BEAST_EXPECT(maxInt64 == "9223372036854775807"); // Naming things is hard - auto const maxInt64Plus1 = std::to_string( - static_cast(std::numeric_limits::max()) + 1); - BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808"); + auto const maxInt64Plus2 = std::to_string( + static_cast(std::numeric_limits::max()) + 2); + BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809"); auto const initialXRP = to_string(kInitialXrp); BEAST_EXPECT(initialXRP == "100000000000000000"); @@ -5839,15 +5839,15 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; + tx[sfAssetsMaximum] = maxInt64Plus2; env(tx, Ter(tefEXCEPTION)); env.close(); // This value will be rounded - auto const insertAt = maxInt64Plus1.size() - 3; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 1000 - BEAST_EXPECT(decimalTest == "9223372036854775.808"); + auto const insertAt = maxInt64Plus2.size() - 3; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+2) / 1000 + BEAST_EXPECT(decimalTest == "9223372036854775.809"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); env(tx); @@ -5891,15 +5891,15 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; + tx[sfAssetsMaximum] = maxInt64Plus2; env(tx, Ter(tefEXCEPTION)); env.close(); // This value will be rounded - auto const insertAt = maxInt64Plus1.size() - 1; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 - BEAST_EXPECT(decimalTest == "922337203685477580.8"); + auto const insertAt = maxInt64Plus2.size() - 1; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+2) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.9"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); env(tx); @@ -5936,7 +5936,7 @@ class Vault_test : public beast::unit_test::Suite env(tx); env.close(); - tx[sfAssetsMaximum] = maxInt64Plus1; + tx[sfAssetsMaximum] = maxInt64Plus2; env(tx); env.close(); @@ -5948,10 +5948,10 @@ class Vault_test : public beast::unit_test::Suite // These values will be rounded to 15 significant digits { - auto const insertAt = maxInt64Plus1.size() - 1; - auto const decimalTest = maxInt64Plus1.substr(0, insertAt) + "." + - maxInt64Plus1.substr(insertAt); // (max int64+1) / 10 - BEAST_EXPECT(decimalTest == "922337203685477580.8"); + auto const insertAt = maxInt64Plus2.size() - 1; + auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." + + maxInt64Plus2.substr(insertAt); // (max int64+2) / 10 + BEAST_EXPECT(decimalTest == "922337203685477580.9"); tx[sfAssetsMaximum] = decimalTest; auto const newKeylet = keylet::vault(owner.id(), env.seq(owner)); env(tx); diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 0e4687f3ba3..92b273eefd3 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -188,6 +188,8 @@ class Number_test : public beast::unit_test::Suite auto const scale = Number::getMantissaScale(); testcase << "test_add " << to_string(scale); + BEAST_EXPECT(Number::getround() == Number::RoundingMode::ToNearest); + using Case = std::tuple; auto const cSmall = std::to_array({ {Number{1'000'000'000'000'000, -15}, @@ -299,12 +301,15 @@ class Number_test : public beast::unit_test::Suite auto const cLargeLegacy = std::to_array({ {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__}, }); - auto const cLargeCorrected = std::to_array({ + auto const cLarge320 = std::to_array({ {Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}, __LINE__}, }); + auto const cLargeCorrected = std::to_array({ + {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep}, __LINE__}, + }); auto test = [this](auto const& c) { for (auto const& [x, y, z, line] : c) { @@ -325,6 +330,10 @@ class Number_test : public beast::unit_test::Suite { test(cLargeLegacy); } + else if (scale == MantissaRange::MantissaScale::Large320) + { + test(cLarge320); + } else { test(cLargeCorrected); @@ -373,7 +382,7 @@ class Number_test : public beast::unit_test::Suite Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -30}, __LINE__}}); - auto const cLarge = std::to_array( + auto const cLargeAll = std::to_array( // Note that items with extremely large mantissas need to be // calculated, because otherwise they overflow uint64. Items from C // with larger mantissa @@ -420,16 +429,55 @@ class Number_test : public beast::unit_test::Suite Number{1'000'000'000'000'000'000, -36}, __LINE__}, {Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__}, - {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, - Number{1, 0}, - Number{(Number::kMaxRep / 10) + 1, 1}, - __LINE__}, - {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, - Number{3, 0}, - Number{Number::kMaxRep}, - __LINE__}, - {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, }); + // Note that items with extremely large mantissas need to be + // calculated, because otherwise they overflow uint64. Items from C + // with larger mantissa + auto const cLarge = std::to_array({ + // Anything larger than kMaxRep rounds up + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + {Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 2, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__}, + }); + auto const cLarge330 = std::to_array({ + // kMaxRep + 1 is below the half-way point, so it rounds down to kMaxRep when the Number + // is created. + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{Number::kMaxRep - 1}, + __LINE__}, + {Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep - 3}, + __LINE__}, + // kMaxRepUp -1 is above the half-way point, so it rounds up to kMaxRepUp when the + // Number is created. Subtracting 1 from that rounds up again. A little non-intuitive. + {Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}}, + Number{1, 0}, + Number{(Number::kMaxRep / 10) + 1, 1}, + __LINE__}, + // Subtracting 3 gets back down to kMaxRep + {Number{false, Number::kMaxRepUp - 1, 0, Number::Normalized{}}, + Number{3, 0}, + Number{Number::kMaxRep}, + __LINE__}, + // 2^63 is the same as kMaxRep+1 + {power(2, 63), Number{3, 0}, Number{Number::kMaxRep - 3}, __LINE__}, + }); auto test = [this](auto const& c) { for (auto const& [x, y, z, line] : c) { @@ -439,13 +487,23 @@ class Number_test : public beast::unit_test::Suite expect(result == z, ss.str(), __FILE__, line); } }; - if (scale == MantissaRange::MantissaScale::Small) - { - test(cSmall); - } - else + switch (scale) { - test(cLarge); + case MantissaRange::MantissaScale::Small: + test(cSmall); + break; + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large320: + test(cLargeAll); + test(cLarge); + break; + case MantissaRange::MantissaScale::Large330: + test(cLargeAll); + test(cLarge330); + break; + default: + BEAST_EXPECT(false); + break; } } @@ -1345,38 +1403,38 @@ class Number_test : public beast::unit_test::Suite auto const scale = Number::getMantissaScale(); testcase << "testToString " << to_string(scale); - auto test = [this](Number const& n, std::string const& expected) { + auto test = [this](Number const& n, std::string const& expected, int line) { auto const result = to_string(n); std::stringstream ss; ss << "to_string(" << result << "). Expected: " << expected; - BEAST_EXPECTS(result == expected, ss.str()); + expect(result == expected, ss.str(), __FILE__, line); }; - test(Number(-2, 0), "-2"); - test(Number(0, 0), "0"); - test(Number(2, 0), "2"); - test(Number(25, -3), "0.025"); - test(Number(-25, -3), "-0.025"); - test(Number(25, 1), "250"); - test(Number(-25, 1), "-250"); - test(Number(2, 20), "2e20"); - test(Number(-2, -20), "-2e-20"); + test(Number(-2, 0), "-2", __LINE__); + test(Number(0, 0), "0", __LINE__); + test(Number(2, 0), "2", __LINE__); + test(Number(25, -3), "0.025", __LINE__); + test(Number(-25, -3), "-0.025", __LINE__); + test(Number(25, 1), "250", __LINE__); + test(Number(-25, 1), "-250", __LINE__); + test(Number(2, 20), "2e20", __LINE__); + test(Number(-2, -20), "-2e-20", __LINE__); // Test the edges // ((exponent < -(25)) || (exponent > -(5))))) // or ((exponent < -(28)) || (exponent > -(8))))) - test(Number(2, -10), "0.0000000002"); - test(Number(2, -11), "2e-11"); + test(Number(2, -10), "0.0000000002", __LINE__); + test(Number(2, -11), "2e-11", __LINE__); - test(Number(-2, 10), "-20000000000"); - test(Number(-2, 11), "-2e11"); + test(Number(-2, 10), "-20000000000", __LINE__); + test(Number(-2, 11), "-2e11", __LINE__); switch (scale) { case MantissaRange::MantissaScale::Small: - test(Number::min(), "1e-32753"); - test(Number::max(), "9999999999999999e32768"); - test(Number::lowest(), "-9999999999999999e32768"); + test(Number::min(), "1e-32753", __LINE__); + test(Number::max(), "9999999999999999e32768", __LINE__); + test(Number::lowest(), "-9999999999999999e32768", __LINE__); { NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); @@ -1384,61 +1442,132 @@ class Number_test : public beast::unit_test::Suite BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999); test( Number{false, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, - "9999999999999999"); + "9999999999999999", + __LINE__); test( Number{true, (maxMantissa * 1000) + 999, -3, Number::Normalized()}, - "-9999999999999999"); + "-9999999999999999", + __LINE__); - test(Number{std::numeric_limits::max(), -3}, "9223372036854775"); + test( + Number{std::numeric_limits::max(), -3}, + "9223372036854775", + __LINE__); test( -(Number{std::numeric_limits::max(), -3}), - "-9223372036854775"); + "-9223372036854775", + __LINE__); test( - Number{std::numeric_limits::min(), 0}, "-9223372036854775e3"); + Number{std::numeric_limits::min(), 0}, + "-9223372036854775e3", + __LINE__); test( -(Number{std::numeric_limits::min(), 0}), - "9223372036854775e3"); + "9223372036854775e3", + __LINE__); } break; default: // Test the edges // ((exponent < -(28)) || (exponent > -(8))))) - test(Number::min(), "1e-32750"); - test(Number::max(), "9223372036854775807e32768"); - test(Number::lowest(), "-9223372036854775807e32768"); + test(Number::min(), "1e-32750", __LINE__); + test(Number::max(), "9223372036854775807e32768", __LINE__); + test(Number::lowest(), "-9223372036854775807e32768", __LINE__); { NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero); auto const maxMantissa = Number::maxMantissa(); BEAST_EXPECT(maxMantissa == 9'999'999'999'999'999'999ULL); test( - Number{false, maxMantissa, 0, Number::Normalized{}}, "9999999999999999990"); + Number{false, maxMantissa, 0, Number::Normalized{}}, + "9999999999999999990", + __LINE__); test( - Number{true, maxMantissa, 0, Number::Normalized{}}, "-9999999999999999990"); + Number{true, maxMantissa, 0, Number::Normalized{}}, + "-9999999999999999990", + __LINE__); test( - Number{std::numeric_limits::max(), 0}, "9223372036854775807"); + Number{std::numeric_limits::max(), 0}, + "9223372036854775807", + __LINE__); test( -(Number{std::numeric_limits::max(), 0}), - "-9223372036854775807"); + "-9223372036854775807", + __LINE__); - // Because the absolute value of min is larger than max, it - // will be scaled down to fit under max. Since we're - // rounding towards zero, the 8 at the end is dropped. - test( - Number{std::numeric_limits::min(), 0}, - "-9223372036854775800"); - test( - -(Number{std::numeric_limits::min(), 0}), - "9223372036854775800"); + switch (scale) + { + case MantissaRange::MantissaScale::Large330: + // Because the absolute value of min() is larger than max(), it + // will be rounded down toward max() + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775807", + __LINE__); + break; + default: + // Because the absolute value of min() is larger than max(), it + // will be scaled down to fit under max(). Since we're + // rounding towards zero, the 8 at the end is dropped. + test( + Number{std::numeric_limits::min(), 0}, + "-9223372036854775800", + __LINE__); + test( + -(Number{std::numeric_limits::min(), 0}), + "9223372036854775800", + __LINE__); + break; + } } + switch (scale) + { + case MantissaRange::MantissaScale::Large330: + // Rounding to nearest, since the mantissa is below the halfway point from + // kMaxRep to kMaxRep up, it will be rounded down to kMaxRep + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775807", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775807", + __LINE__); + break; + default: + // Rounding to nearest, since the mantissa is bigger than kMaxRep, the 8 + // will be dropped, and since that is bigger than 5, the result will be + // rounded up from 0 to 1. + test( + Number{std::numeric_limits::max(), 0} + 1, + "9223372036854775810", + __LINE__); + test( + -(Number{std::numeric_limits::max(), 0} + 1), + "-9223372036854775810", + __LINE__); + break; + } + // Rounding to nearest, will be rounded up to kMaxRepUp, but for different reasons + // depending on the scale. If older than "Large", it rounds up for the same reason + // "+1" rounds up. For "Large", since the mantissa is above the halfway point from + // kMaxRep to kMaxRepUp, it will be rounded up to kMaxRepUp. test( - Number{std::numeric_limits::max(), 0} + 1, "9223372036854775810"); + Number{std::numeric_limits::max(), 0} + 2, + "9223372036854775810", + __LINE__); test( - -(Number{std::numeric_limits::max(), 0} + 1), - "-9223372036854775810"); + -(Number{std::numeric_limits::max(), 0} + 2), + "-9223372036854775810", + __LINE__); + break; } } @@ -1776,7 +1905,7 @@ class Number_test : public beast::unit_test::Suite } void - testUpwardRoundsDown() + testEdgeCases() { auto const scale = Number::getMantissaScale(); { @@ -1800,15 +1929,14 @@ class Number_test : public beast::unit_test::Suite BigInt const signedDifference = storedValue - exactProduct; - log << "\n" - << " a = " << fmt(BigInt(kAValue)) << "\n" + log << " a = " << fmt(BigInt(kAValue)) << "\n" << " b = " << fmt(BigInt(kBValue)) << "\n" << " exact a*b = " << fmt(exactProduct) << "\n" << " stored = " << fmt(storedValue) << "\n" << " stored - exact = " << fmt(signedDifference) << "\n" << " upward = " << (signedDifference >= 0 ? "held" : "VIOLATED") << "\n" << " stored.mantissa = " << product.mantissa() << "\n" - << " stored.exponent = " << product.exponent() << "\n"; + << " stored.exponent = " << product.exponent() << "\n\n"; log.flush(); switch (scale) @@ -1882,15 +2010,14 @@ class Number_test : public beast::unit_test::Suite dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); dec const diff = stored - exact; - log << "\n" - << " a = " << aValue << "\n" + log << " a = " << aValue << "\n" << " b = " << bValue << "\n" << " exact a/b = " << fmt(exact) << "\n" << " stored a/b = " << fmt(stored) << "\n" << " stored - exact = " << fmt(diff) << " (negative => Upward gave value BELOW truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; + << " quotient.exponent = " << quotient.exponent() << "\n\n"; log.flush(); // Upward invariant: stored >= exact. Bug: stored < exact. @@ -1933,15 +2060,14 @@ class Number_test : public beast::unit_test::Suite dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); dec const diff = stored - exact; - log << "\n" - << " a = " << aValue << "\n" + log << " a = " << aValue << "\n" << " b = " << bValue << "\n" << " exact a/b = " << fmt(exact) << "\n" << " stored a/b = " << fmt(stored) << "\n" << " stored - exact = " << fmt(diff) << " (positive => Downward gave value ABOVE truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; + << " quotient.exponent = " << quotient.exponent() << "\n\n"; log.flush(); // invariant: stored <= exact. Bug: stored > exact. @@ -1991,15 +2117,14 @@ class Number_test : public beast::unit_test::Suite dec const stored = dec(quotient.mantissa()) * pow10(quotient.exponent()); dec const diff = stored - exact; - log << "\n" - << " a = " << aValue << "\n" + log << " a = " << aValue << "\n" << " b = " << bValue << "\n" << " exact a/b = " << fmt(exact) << "\n" << " stored a/b = " << fmt(stored) << "\n" << " stored - exact = " << fmt(diff) << " (negative => ToNearest gave value BELOW truth)\n" << " quotient.mantissa = " << quotient.mantissa() << "\n" - << " quotient.exponent = " << quotient.exponent() << "\n"; + << " quotient.exponent = " << quotient.exponent() << "\n\n"; log.flush(); // invariant: stored >= exact. Bug: stored < exact. @@ -2086,7 +2211,7 @@ class Number_test : public beast::unit_test::Suite return sums; }(); - log << "\n a = " << a << " (" << fmt(bigA) + log << " a = " << a << " (" << fmt(bigA) << ")\n b = " << b << " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n"; for (auto const& [r, sum] : sums) @@ -2096,6 +2221,7 @@ class Number_test : public beast::unit_test::Suite log << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first) << "\n difference = " << fmt(diff) << "\n"; } + log << "\n"; log.flush(); auto const expectedExponent = @@ -2146,6 +2272,83 @@ class Number_test : public beast::unit_test::Suite } } } + + { + testcase << "normalization cusp: ToNearest and Downward behavior " << to_string(scale); + constexpr auto kMaxRep = Number::kMaxRep; + + // Both ToNearest and Downward should round to `below` + auto constexpr actual = static_cast(kMaxRep) + 1; + Number const below{static_cast(kMaxRep), 0}; + Number const above{ + false, static_cast(kMaxRep) + 3, 0, Number::Normalized{}}; + + auto construct = [](Number::RoundingMode mode) { + NumberRoundModeGuard const roundGuard{mode}; + return Number(false, actual, 0, Number::Normalized{}); + }; + Number const upward = construct(Number::RoundingMode::Upward); + + Number const toNearest = construct(Number::RoundingMode::ToNearest); + + Number const downward = construct(Number::RoundingMode::Downward); + + log << " actual = " << actual << " (kMaxRep + 1)\n" + << " below = " << below << " (kMaxRep, distance 1)\n" + << " above = " << above << " (kMaxRep + 3, distance 2)\n" + << " Upward = " << upward << "\n" + << " ToNearest = " << toNearest << "\n" + << " Downward = " << downward << "\n\n"; + log.flush(); + + switch (scale) + { + case MantissaRange::MantissaScale::Small: + // With the small mantissa, everything but Downward rounds UP, including the + // reference values, "above" and "below" + + BEAST_EXPECT(below == above); + BEAST_EXPECT(upward == above); + BEAST_EXPECT(toNearest == above); + + BEAST_EXPECT(downward < below); + + break; + + case MantissaRange::MantissaScale::LargeLegacy: + case MantissaRange::MantissaScale::Large320: + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds UP when the DOWN neighbor is strictly closer + BEAST_EXPECT(toNearest == above); + BEAST_EXPECT(toNearest > below); + + // Downward undershoots: it returns a value below `below` + BEAST_EXPECT(downward < below); + + // Both should have given the same answer, but they differ + BEAST_EXPECT(toNearest > downward); + + break; + default: + // Covers "Large" and any newly added scales + + // Upward round UP + BEAST_EXPECT(upward == above); + + // ToNearest rounds to the strictly closer DOWN neighbor + BEAST_EXPECT(toNearest != above); + BEAST_EXPECT(toNearest == below); + + // Downward also rounds to `below` + BEAST_EXPECT(downward == below); + + // ToNearest rounds to downward + BEAST_EXPECT(toNearest == downward); + break; + } + } } void @@ -2359,6 +2562,166 @@ class Number_test : public beast::unit_test::Suite } } + void + testNumberRoundCuspWithFractionalParts() + { + auto const scale = Number::getMantissaScale(); + + testcase << "normalization cusp: rounding behavior with fractional parts " + << to_string(scale); + NumberRoundModeGuard const roundGuard{Number::RoundingMode::ToNearest}; + + Number const below{static_cast(Number::kMaxRep), 0}; + Number const above{false, Number::kMaxRepUp, 0, Number::Normalized{}}; + + log << "Below: " << below << ", Above: " << above << "\n"; + + auto const zeroPointFour = Number(4, -1); + auto const zeroPointSix = Number(6, -1); + auto const onePointFour = Number(14, -1); + auto const onePointFive = Number(15, -1); + auto const onePointSix = Number(16, -1); + auto const twoPointFour = Number(24, -1); + auto const twoPointSix = Number(26, -1); + + auto const operands = std::to_array({ + zeroPointFour, + zeroPointSix, + onePointFour, + onePointFive, + onePointSix, + twoPointFour, + twoPointSix, + }); + + auto const modes = std::to_array({ + Number::RoundingMode::ToNearest, + Number::RoundingMode::TowardsZero, + Number::RoundingMode::Downward, + Number::RoundingMode::Upward, + }); + + // Addition cases test kMaxRep + Operand + for (auto const& mode : modes) + { + for (auto const& operand : operands) + { + NumberRoundModeGuard const rg{mode}; + + auto const expectedValue = [&]() { + if (scale >= MantissaRange::MantissaScale::Large330) + { + if (mode == Number::RoundingMode::ToNearest && operand < onePointFive) + return below; + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + return below; + } + if (scale == MantissaRange::MantissaScale::Large320) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand < zeroPointSix) + return below; + } + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + { + if (operand >= onePointFour) + return below - 7; + return below; + } + } + if (scale == MantissaRange::MantissaScale::LargeLegacy) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand < zeroPointSix) + return below; + if (operand == zeroPointSix) + return below - 7; + } + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + { + if (operand >= onePointFour) + return below - 7; + return below; + } + if (mode == Number::RoundingMode::Upward && operand <= zeroPointSix) + return below - 7; + } + if (scale == MantissaRange::MantissaScale::Small && + mode == Number::RoundingMode::Upward) + return above + 1000; + return above; + }(); + + Number const actual = below + operand; + + std::stringstream ss; + ss << "kMaxRep + " << operand << " rounded " << to_string(mode) << " to " << actual + << ". Expected: " << expectedValue; + BEAST_EXPECTS(actual == expectedValue, ss.str()); + } + } + + // Subtraction cases test kMaxRepUp - Operand + for (auto const& mode : modes) + { + for (auto const& operand : operands) + { + NumberRoundModeGuard const rg{mode}; + + auto const expectedValue = [&]() { + if (scale >= MantissaRange::MantissaScale::Large330) + { + if (mode == Number::RoundingMode::ToNearest && operand > onePointFive) + return below; + if (mode == Number::RoundingMode::TowardsZero || + mode == Number::RoundingMode::Downward) + return below; + } + if (scale == MantissaRange::MantissaScale::LargeLegacy || + scale == MantissaRange::MantissaScale::Large320) + { + if (mode == Number::RoundingMode::ToNearest) + { + if (operand >= twoPointSix) + return below; + } + if (mode == Number::RoundingMode::TowardsZero) + { + if (operand >= onePointFour) + return below - 7; + } + if (mode == Number::RoundingMode::Downward) + { + if (operand <= onePointSix) + return below - 7; + return below; + } + } + if (scale == MantissaRange::MantissaScale::Small) + { + if (mode == Number::RoundingMode::Downward) + return below - 1000; + if (mode == Number::RoundingMode::Upward) + return below; + } + return above; + }(); + + Number const actual = above - operand; + + std::stringstream ss; + ss << "kMaxRepUp - " << operand << " rounded " << to_string(mode) << " to " + << actual << ". Expected: " << expectedValue; + BEAST_EXPECTS(actual == expectedValue, ss.str()); + } + } + } + void run() override { @@ -2388,9 +2751,10 @@ class Number_test : public beast::unit_test::Suite testRounding(); testInt64(); - testUpwardRoundsDown(); + testEdgeCases(); testNumberAddDirectedSignWrong(); testNumberAddToNearestPicksFarther(); + testNumberRoundCuspWithFractionalParts(); } } };