Compare commits

...

73 Commits

Author SHA1 Message Date
Ed Hennis
84c9dbafbe [WIP] Checkpoint 2026-06-03 18:37:53 -04:00
Ed Hennis
51902cd6b4 Revert "Remove the kMaxRep+1 rounding tests"
This reverts commit c84939ccea.
2026-06-02 16:03:08 -04:00
Ed Hennis
c84939ccea Remove the kMaxRep+1 rounding tests 2026-06-02 15:59:26 -04:00
Ed Hennis
9e8c3caef4 Improve accuracy of Number::operator+=
- Use more of the available range of the uint128 operands.
- Also refactor Number::Guard::round() to return an enum.
2026-06-02 15:35:42 -04:00
Ed Hennis
35bee87909 Experimental: Scale addition operands up to preserve accuracy 2026-06-01 21:52:31 -04:00
Ed Hennis
261508a0ec Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-round-maxrep-down
* XRPLF/develop:
  ci: Check binaries separately from building them (7355)
  ci: [DEPENDABOT] bump eps1lon/actions-label-merge-conflict from 3.0.3 to 3.1.0 (7375)
  refactor: Use `STLedgerEntry` type aliases instead of `std::shared_ptr` (7282)
  fix: Adjust xrpld systemd service and update timer (7374)
  release: Bump version to 3.2.0-rc3 (7371)
  fix: Pin overpayment principal reduction to exact on-grid value (7360)
  fix: Improve upward rounding edge cases for Number::operator/= (7328)
  refactor: Revert "perf: Remove unnecessary caches (5439)" (7359)
  fix: Add zero domainID check for permissionedDomain (7362)
2026-06-01 15:33:34 -04:00
Ed Hennis
9f872f2179 Include upward, write tests based on "expected" behavior 2026-05-29 19:46:50 -04:00
Ed Hennis
d1af39d9dd test: Add another rounding unit test 2026-05-29 19:03:34 -04:00
Ed Hennis
f6a26ca34f CI feedback: Add test cases covering other rounding modes
- Downward with a negative result, and ToNearest with a remainder
  slightly larger than 0.5.
2026-05-29 18:54:08 -04:00
Ed Hennis
c0569037f8 Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-29 16:34:51 -04:00
Ed Hennis
be9ae88d48 Accept AI suggestion
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 23:38:54 -04:00
Ed Hennis
cd21d74538 Update the testUpwardRoundsDown test to run under all scales
- Demonstrates the incorrect "before" behavior
2026-05-28 19:58:52 -04:00
Ed Hennis
2fdfd2b420 Review feedback from Tapanito and AI
- Add missing headers.
- Improve code coverage exclusions.
- Clean up several variable names.
- Improve explanatory comments.
- Remove the switch statement from MantissaRange::getMin. Change it to
  a straight power of ten lookup.
2026-05-28 18:41:19 -04:00
Ed Hennis
06a3f76ccd Skip clang-tidy false positive: misc-include-cleaner 2026-05-28 17:47:41 -04:00
Ed Hennis
dadf4d737d Merge branch 'develop' into ximinez/number-division-accuracy 2026-05-28 17:46:40 -04:00
Ed Hennis
7b66b42713 Merge branch 'develop' into ximinez/number-division-accuracy 2026-05-27 17:00:59 -04:00
Ed Hennis
18ac8a0583 Merge branch 'develop' into ximinez/number-division-accuracy 2026-05-27 15:18:17 -04:00
Ed Hennis
de2efa5cb9 Remove TMax entirely from normalizeToRange; check type matching directly 2026-05-27 13:06:52 -04:00
Ed Hennis
8dcd88e83c Tweak how the denominator is handled in division
- Removes one int64 to 128 conversion
2026-05-27 12:34:49 -04:00
Ed Hennis
5333422402 Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-division-accuracy
* XRPLF/develop:
  fix: Fix a rounding error at the `Number::maxRep` cusp (7051)
2026-05-27 12:17:30 -04:00
Ed Hennis
4ec049e727 Minor fixes: missing include, variable init, typo 2026-05-27 12:09:13 -04:00
Ed Hennis
ae9c72bb7c Test optimization: Number_test::pow10 2026-05-27 00:36:38 -04:00
Ed Hennis
5abecb9fcb Significant rewrite
- Simplify Number::operator/= to use more constexpr values, and fewer
  variations.
  - Most significantly, rounding up doesn't need more precision, it only
    needs to know if there's a remainder after the current precision work
    is done. Tracked similarly Guard::xbit_.
- Build a constexpr lookup array for powers of 10. Only a handful of
  values are used, but since it's built at compile time, and constexpr,
  unused values do not affect memory or performance.
2026-05-27 00:07:48 -04:00
Ed Hennis
12670b0c3f Merge branch 'ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy 2026-05-26 19:21:05 -04:00
Ed Hennis
1e7876a03c Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 19:21:01 -04:00
Ed Hennis
e851e80de0 Merge branch 'ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy 2026-05-26 16:56:47 -04:00
Ed Hennis
a963035f76 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 16:56:43 -04:00
Ed Hennis
8ab904de57 Merge branch 'ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy 2026-05-26 16:01:44 -04:00
Ed Hennis
100ec464d9 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 16:01:40 -04:00
Ed Hennis
e89e6f50e8 Merge remote-tracking branch 'XRPLF/ximinez/number-fix-maxrepcusp' into ximinez/number-division-accuracy
* XRPLF/ximinez/number-fix-maxrepcusp:
  clang-tidy: implicit bool conversion
  Address some AI review feedback: predeclare, include, format, comment
  fix: Fix `VaultInvariant` and `VaultDeposit` precision bugs at IOU scale boundaries (7272)
  ci: Add clang to nix images (7308)
  fix: Include management-fee delta in doOverpayment assertion (7039)
  fix: Fix clang-tidy pre-commit hook to locate compile_commands.json from repo root (7325)
  fix: Use consistent scale for `debtTotal` (7093)
  fix: Skip deleted book directories and non-root modifications in `ValidBookDirectory` invariant (7312)
  fix: Address review feedback on FD/handle guarding (5823 follow-up) (7310)
  fix: Fix non-canonical MPT amount (7117)
2026-05-26 15:53:36 -04:00
Ed Hennis
27456fa439 Use the local range instead of calling a function 2026-05-26 15:52:25 -04:00
Ed Hennis
d6844311c0 clang-tidy: missing header 2026-05-26 15:48:29 -04:00
Ed Hennis
fbee0349f5 clang-tidy: implicit bool conversion 2026-05-26 15:21:42 -04:00
Ed Hennis
84ca271d95 Address some AI review feedback: predeclare, include, format, comment
- Predeclare type reference in Rules.h
- Remove an unneeded include in EscrowToken_test
- Number_test will format negative BigInts correctly (unused)
- Remove an inaccurate comment
2026-05-26 13:51:06 -04:00
Ed Hennis
75dfc65f5f Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-26 13:47:33 -04:00
Ed Hennis
48b1716e6f Make Number::operator/= significantly more accurate
- Prevents extreme dust rounding from getting lost, especially when
  rounding away from zero. (Upward for positive, downward for negative.)
2026-05-23 19:02:03 +01:00
Ed Hennis
4ab886bcbc Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-22 17:56:15 -04:00
Ed Hennis
7f64c337d8 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-21 14:25:50 -04:00
Ed Hennis
61bdd6fb78 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-21 10:10:00 -04:00
Ed Hennis
8e06e78f11 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-21 07:11:49 -04:00
Ed Hennis
42fda85fbc Fix more AMM tests, and to not exclude fixCleanup3_2_0 2026-05-21 12:04:29 +01:00
Ed Hennis
3a4b92b050 Change the priority of the amendments for large mantissas
- Order the checks so that large mantissa is only enabled if SAV or LP
  are enabled. fixCleanup3_2_0 only enables the rounding fix.
- Fix tests, and don't exclude fixCleanup3_2_0 in AMM tests
- Also fix formatting
2026-05-21 00:53:07 +01:00
Ed Hennis
aea19df3c1 Apply suggestions from @Tapanito code review
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-05-20 18:46:17 -04:00
Ed Hennis
8b56749ca3 Apply suggestions from Copilot code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-20 18:39:46 -04:00
Ed Hennis
71cf996fc6 Review feedback from @tapanito: lambda checks condition in doRoundUp 2026-05-20 23:28:26 +01:00
Ed Hennis
4c7ea64b6c Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-19 16:53:33 -04:00
Ed Hennis
c8947c6f75 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-19 10:15:10 -04:00
Ed Hennis
09ae5b719f Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-19 05:15:47 -04:00
Ed Hennis
09f2d06dd4 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-15 21:32:09 -04:00
Ed Hennis
6964013941 Merge remote-tracking branch 'XRPLF/develop' into ximinez/number-fix-maxrepcusp
* XRPLF/develop:
  release: Set version to 3.3.0-b0 (7280)
  refactor: Rename static constants (7120)
  refactor: Use `isFlag` where possible instead of bitwise math (7278)
  ci: Update XRPLF/actions (7281)
2026-05-15 21:14:34 -04:00
Ed Hennis
70c6e01d7e clang-tidy: this is ridiculous 2026-05-14 21:02:49 -04:00
Ed Hennis
ddfb7ee69c clang-tidy: {} 2026-05-14 20:48:34 -04:00
Ed Hennis
b69b9242e2 fixup! clang-tidy: Avoid nested "?:" in global Rules initialization 2026-05-14 20:33:15 -04:00
Ed Hennis
cd2fcf0a5e Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-14 18:25:17 -04:00
Ed Hennis
69656d6b67 clang-tidy: Avoid nested "?:" in global Rules initialization 2026-05-14 18:24:05 -04:00
Ed Hennis
46b946b22e clang-tidy: missing include 2026-05-13 22:32:37 -04:00
Ed Hennis
ae03b30f29 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-13 20:16:19 -04:00
Ed Hennis
4c7c019add Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-13 12:03:35 -04:00
Ed Hennis
47f30c913d Fix broken unit tests: EscrowToken
- Some EscrowToken tests used a hard-coded list of amendments to
  determine whether to expect large mantissa logic. That ignored the
  effects of fixCleanup3_2_0, especially as applied to the previous fix
  affecting preflight, preclaim, etc.
- Add a helper function, useRulesGuards, which will return the same
  decision as createGuards and setCurrentRulesImpl. Use that helper
  function in the relevant tests.
- Also remove an #include that clang-tidy was complaining about.
2026-05-12 21:26:54 -04:00
Ed Hennis
d7d5b83f6d Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-12 19:18:45 -04:00
Ed Hennis
e22938d69f AI review feedback: createGuards
- Refactor the Guard decision in withTxnType into createGuards, which
  lives in Rules.cpp. It is physically located near
  setCurrentTransactionRules, and documented to explain that changes
  need to be synchronized.
2026-05-12 18:44:05 -04:00
Ed Hennis
7c9a56ff24 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-12 16:26:09 -04:00
Ed Hennis
5a40416673 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-12 15:59:19 -04:00
Ed Hennis
30334cd1f4 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-11 13:34:11 -04:00
Ed Hennis
5558e1b522 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-07 18:10:03 -04:00
Ed Hennis
cd0f49a003 Address more nitpicky AI comments 2026-05-07 17:05:10 -04:00
Ed Hennis
22d2703ce8 What is it going to take to get clang-tidy to shut up? 2026-05-07 16:50:43 -04:00
Ed Hennis
d03274b731 Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-07 14:18:28 -04:00
Ed Hennis
1b6047afe1 More clang-tidy changes: AMMExtended_test member and Number_test includes 2026-05-07 13:51:48 -04:00
Ed Hennis
175a04160d Merge branch 'develop' into ximinez/number-fix-maxrepcusp 2026-05-07 13:28:33 -04:00
Ed Hennis
b050c151f8 Fix clang-tidy issues, and more AI complaints 2026-05-06 23:26:11 -04:00
Ed Hennis
a2b21d75ce Fix AI-identified mistakes 2026-05-06 22:56:07 -04:00
Ed Hennis
b40d2a8e7d fix: Fix a rounding error at the Number::maxRep cusp
- Add helper function, doDropDigit, to wrap the common pattern:
    push(mantissa % 10);
    mantissa /= 10;
    ++exponent;
- Might have been helpful to catch this issue when developing.
2026-05-06 22:49:44 -04:00
3 changed files with 469 additions and 161 deletions

View File

@@ -51,37 +51,43 @@ namespace detail {
* compile time. Doing it at runtime would be pretty wasteful and
* inefficient.
*/
constexpr std::size_t kInt64Digits = 20;
consteval std::array<std::uint64_t, kInt64Digits>
constexpr std::size_t kUint64Digits = 20;
constexpr std::size_t kUint128Digits = 39;
template <typename T, std::size_t digits>
consteval std::array<T, digits>
buildPowersOfTen()
{
std::array<std::uint64_t, kInt64Digits> result{};
std::array<T, digits> result{};
std::uint64_t power = 1;
T power = 1;
std::size_t exponent = 0;
// end the loop early so it doesn't overflow;
for (; exponent < result.size() - 1; ++exponent, power *= 10)
{
result[exponent] = power;
if (power > std::numeric_limits<std::uint64_t>::max() / 10)
if (power > std::numeric_limits<T>::max() / 10)
throw std::logic_error("Power of 10 table is too big");
}
result[exponent] = power;
if (power < std::numeric_limits<std::uint64_t>::max() / 10)
throw std::logic_error("Power of 10 table is not big enough for the uint64_t type");
if (power < std::numeric_limits<T>::max() / 10)
throw std::logic_error("Power of 10 table is not big enough for the given type");
return result;
}
} // namespace detail
constexpr std::array<std::uint64_t, detail::kInt64Digits> kPowerOfTen = detail::buildPowersOfTen();
template <typename T = std::uint64_t, std::size_t digits = detail::kUint64Digits>
constexpr std::array<T, digits> kPowerOfTenImpl = detail::buildPowersOfTen<T, digits>();
constexpr auto kPowerOfTen = kPowerOfTenImpl<std::uint64_t, detail::kUint64Digits>;
static_assert(kPowerOfTen[0] == 1);
static_assert(kPowerOfTen[1] == 10);
static_assert(kPowerOfTen[10] == 10'000'000'000);
static_assert(
isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kInt64Digits - 1);
isPowerOfTen(kPowerOfTen.back()) && *logTen(kPowerOfTen.back()) == detail::kUint64Digits - 1);
/** MantissaRange defines a range for the mantissa of a normalized Number.
*
@@ -141,7 +147,7 @@ struct MantissaRange final
int const log{getExponent(scale)};
rep const min{getMin(scale, log)};
rep const max{(min * 10) - 1};
CuspRoundingFix const cuspRoundingFixEnabled{isCuspFixEnabled(scale)};
CuspRoundingFix const cuspRoundingFix{isCuspFixEnabled(scale)};
static MantissaRange const&
getMantissaRange(MantissaScale scale);
@@ -319,6 +325,9 @@ public:
static constexpr internalrep kMaxRep = std::numeric_limits<rep>::max();
static_assert(kMaxRep == 9'223'372'036'854'775'807);
static_assert(-kMaxRep == std::numeric_limits<rep>::min() + 1);
static constexpr internalrep kMaxRepUp =
((static_cast<internalrep>(std::numeric_limits<rep>::max()) / 10) + 1) * 10;
static_assert(kMaxRepUp == 9'223'372'036'854'775'810ULL);
// May need to make unchecked private
struct Unchecked
@@ -560,7 +569,7 @@ private:
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
MantissaRange::CuspRoundingFix cuspRoundingFix);
template <class T>
friend void
@@ -570,7 +579,7 @@ private:
int& exponent,
MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
MantissaRange::CuspRoundingFix cuspRoundingFix,
bool dropped);
[[nodiscard]] bool

View File

@@ -65,7 +65,7 @@ MantissaRange::getRanges()
static_assert(kRange.log == 15);
static_assert(kRange.min < Number::kMaxRep);
static_assert(kRange.max < Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled);
}
{
[[maybe_unused]]
@@ -76,7 +76,7 @@ MantissaRange::getRanges()
static_assert(kRange.log == 18);
static_assert(kRange.min < Number::kMaxRep);
static_assert(kRange.max > Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Disabled);
static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Disabled);
}
{
[[maybe_unused]]
@@ -87,7 +87,7 @@ MantissaRange::getRanges()
static_assert(kRange.log == 18);
static_assert(kRange.min < Number::kMaxRep);
static_assert(kRange.max > Number::kMaxRep);
static_assert(kRange.cuspRoundingFixEnabled == CuspRoundingFix::Enabled);
static_assert(kRange.cuspRoundingFix == CuspRoundingFix::Enabled);
}
return map;
}();
@@ -206,12 +206,6 @@ public:
void
doDropDigit(T& mantissa, int& exponent) noexcept;
// Indicate round direction: 1 is up, -1 is down, 0 is even
// This enables the client to round towards nearest, and on
// tie, round towards even.
[[nodiscard]] int
round() const noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
@@ -221,25 +215,51 @@ public:
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
MantissaRange::CuspRoundingFix cuspRoundingFix,
std::string location);
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
doRoundDown(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
doRoundDown(
bool& negative,
T& mantissa,
int& exponent,
internalrep const& minMantissa);
// Modify the result to the correctly rounded value
void
doRound(rep& drops, std::string location) const;
doRound(rep& drops, MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location);
private:
template <class T>
void
pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix);
enum class Round {
Down = -1,
Even = 0,
Up = 1,
};
// Indicate round direction: 1 is up, -1 is down, 0 is even
// This enables the client to round towards nearest, and on
// tie, round towards even.
[[nodiscard]]
Round
round() const noexcept;
void
doPush(unsigned d) noexcept;
template <UnsignedMantissa T>
void
bringIntoRange(bool& negative, T& mantissa, int& exponent, internalrep const& minMantissa);
bringIntoRange(
bool& negative,
T& mantissa,
int& exponent,
internalrep const& minMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFix);
};
inline void
@@ -310,45 +330,78 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
++exponent;
}
template <class T>
void
Number::Guard::pushOverflow(T const& mantissa, MantissaRange::CuspRoundingFix cuspRoundingFix)
{
XRPL_ASSERT(mantissa <= kMaxRepUp, "xrpl::Number::Guard::doRoundUp : valid mantissa");
if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep &&
mantissa < kMaxRepUp)
{
// Special case rounding rules for the values between kMaxRep and kMaxRepUp.
// Scale the spread between kMaxRep and kMaxRepUp from 0 to 9, and push it onto the guard as
// if it was a digit that got removed, but don't remove it. This method is future-proof in
// case the number of mantissa bits ever changes. Effects:
// * For round to nearest
// * if the mantissa is below the midpoint, it'll round "down" to kMaxRepUp
// * if above the midpoint, it'll round "down" to kMaxRep
// * if can never be exactly at the midpoint, because kMaxRepUp is always even, and
// kMaxRep is always odd, so don't worry about it.
// * For round upward, will round up to kMaxRepUp for positive values, down for negative.
// * For round downward, does the opposite of upward.
// * For round toward zero, always rounds down.
auto constexpr spread = kMaxRepUp - kMaxRep;
static_assert(spread < 10 && spread >= 0);
auto const diff = mantissa - kMaxRep;
auto const digit = (diff * 10) / spread;
XRPL_ASSERT(digit > 0 && digit < 10, "xrpld::Number::Guard::xxxx : 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
int
Number::Guard::round() const noexcept
// 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
{
auto mode = Number::getround();
if (mode == RoundingMode::TowardsZero)
return -1;
return Round::Down;
if (mode == RoundingMode::Downward)
{
if (sbit_)
{
if (digits_ > 0 || xbit_)
return 1;
return Round::Up;
}
return -1;
return Round::Down;
}
if (mode == RoundingMode::Upward)
{
if (sbit_)
return -1;
return Round::Down;
if (digits_ > 0 || xbit_)
return 1;
return -1;
return Round::Up;
return Round::Down;
}
// assume round to nearest if mode is not one of the predefined values
if (digits_ > 0x5000'0000'0000'0000)
return 1;
return Round::Up;
if (digits_ < 0x5000'0000'0000'0000)
return -1;
return Round::Down;
if (xbit_)
return 1;
return 0;
return Round::Up;
return Round::Even;
}
template <UnsignedMantissa T>
@@ -357,16 +410,19 @@ Number::Guard::bringIntoRange(
bool& negative,
T& mantissa,
int& exponent,
internalrep const& minMantissa)
internalrep const& minMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFix)
{
// Bring mantissa back into the minMantissa / maxMantissa range AFTER
// rounding
if (mantissa < minMantissa)
if (mantissa < minMantissa &&
(cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled || mantissa != 0))
{
mantissa *= 10;
--exponent;
}
if (exponent < kMinExponent)
if (exponent < kMinExponent ||
(cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa == 0))
{
static constexpr Number kZero = Number{};
@@ -384,16 +440,18 @@ Number::Guard::doRoundUp(
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
MantissaRange::CuspRoundingFix cuspRoundingFix,
std::string location)
{
auto r = round();
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
pushOverflow(mantissa, cuspRoundingFix);
auto const r = round();
if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1))
{
auto const safeToIncrement = [&maxMantissa](auto const& mantissa) {
return mantissa < maxMantissa && mantissa < kMaxRep;
};
if (cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled)
if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled)
{
// Ensure mantissa after incrementing fits within both the
// min/maxMantissa range and is a valid "rep".
@@ -409,6 +467,8 @@ Number::Guard::doRoundUp(
// 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.
if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep && mantissa < kMaxRepUp)
mantissa = kMaxRepUp;
doDropDigit(mantissa, exponent);
XRPL_ASSERT_PARTS(
safeToIncrement(mantissa),
@@ -420,7 +480,7 @@ Number::Guard::doRoundUp(
exponent,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
cuspRoundingFix,
location);
return;
}
@@ -440,7 +500,11 @@ Number::Guard::doRoundUp(
}
}
}
bringIntoRange(negative, mantissa, exponent, minMantissa);
else if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && mantissa > kMaxRep)
{
mantissa = kMaxRep;
}
bringIntoRange(negative, mantissa, exponent, minMantissa, cuspRoundingFix);
if (exponent > kMaxExponent)
Throw<std::overflow_error>(std::string(location));
}
@@ -453,8 +517,10 @@ Number::Guard::doRoundDown(
int& exponent,
internalrep const& minMantissa)
{
// Do not pushOverflow here.
auto r = round();
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
if (r == Round::Up || (r == Round::Even && (mantissa & 1) == 1))
{
--mantissa;
if (mantissa < minMantissa)
@@ -463,15 +529,17 @@ Number::Guard::doRoundDown(
--exponent;
}
}
bringIntoRange(negative, mantissa, exponent, minMantissa);
bringIntoRange(negative, mantissa, exponent, minMantissa, cuspRoundingFix);
}
// Modify the result to the correctly rounded value
void
Number::Guard::doRound(rep& drops, std::string location) const
Number::Guard::doRound(rep& drops, MantissaRange::CuspRoundingFix cuspRoundingFix, std::string location)
{
pushOverflow(drops, cuspRoundingFix);
auto r = round();
if (r == 1 || (r == 0 && (drops & 1) == 1))
if (r == Round::Up || (r == Round::Even && (drops & 1) == 1))
{
if (drops >= kMaxRep)
{
@@ -486,6 +554,12 @@ Number::Guard::doRound(rep& drops, std::string location) const
}
++drops;
}
else if (cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && drops > kMaxRep)
{
// This will probably be impossible because this function is not called by mutating
// functions, so the Number will already be normalized.
drops = kMaxRep;
}
if (isNegative())
drops = -drops;
}
@@ -530,12 +604,14 @@ doNormalize(
int& exponent,
MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled,
MantissaRange::CuspRoundingFix cuspRoundingFix,
bool dropped)
{
static constexpr auto kMinExponent = Number::kMinExponent;
static constexpr auto kMaxExponent = Number::kMaxExponent;
static constexpr auto kMaxRep = Number::kMaxRep;
auto const kRepLimit = cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled
? Number::kMaxRep
: Number::kMaxRepUp;
using Guard = Number::Guard;
@@ -585,7 +661,7 @@ 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 > kRepLimit)
{
if (exponent >= kMaxExponent)
throw std::overflow_error("Number::normalize 1.5");
@@ -595,7 +671,7 @@ doNormalize(
// 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");
XRPL_ASSERT_PARTS(m <= kRepLimit, "xrpl::doNormalize", "intermediate mantissa fits in limit");
mantissa = m;
g.doRoundUp(
@@ -604,7 +680,7 @@ doNormalize(
exponent,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
cuspRoundingFix,
"Number::normalize 2");
XRPL_ASSERT_PARTS(
mantissa >= minMantissa && mantissa <= maxMantissa,
@@ -620,13 +696,12 @@ Number::normalize<uint128_t>(
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
MantissaRange::CuspRoundingFix cuspRoundingFix)
{
// Not used by every compiler version, and thus not necessarily
// counted by coverage build
// LCOV_EXCL_START
doNormalize(
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false);
// LCOV_EXCL_STOP
}
@@ -638,13 +713,12 @@ Number::normalize<unsigned long long>(
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
MantissaRange::CuspRoundingFix cuspRoundingFix)
{
// Not used by every compiler version, and thus not necessarily
// counted by coverage build
// LCOV_EXCL_START
doNormalize(
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false);
// LCOV_EXCL_STOP
}
@@ -656,16 +730,15 @@ Number::normalize<unsigned long>(
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled)
MantissaRange::CuspRoundingFix cuspRoundingFix)
{
doNormalize(
negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFixEnabled, false);
doNormalize(negative, mantissa, exponent, minMantissa, maxMantissa, cuspRoundingFix, false);
}
void
Number::normalize(MantissaRange const& range)
{
normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFixEnabled);
normalize(negative_, mantissa_, exponent_, range.min, range.max, range.cuspRoundingFix);
}
// Copy the number, but set a new exponent. Because the mantissa doesn't change,
@@ -720,45 +793,77 @@ Number::operator+=(Number const& y)
uint128_t ym = y.mantissa_;
auto ye = y.exponent_;
Guard g;
auto const& range = kRange.get();
// Bring the exponents of both values into agreement, so the mantissas are on the same scale
// and can be added directly together
// expandM / expandE: First try to expand the mantissa and bring the exponent down
// shringM / shrinkE: Then shrink the mantissa and bring the exponent up, if necessary
auto const adjust = [&g, &range](
uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) {
constexpr uint128_t kSafeLimit = kPowerOfTenImpl<uint128_t, detail::kUint128Digits>[37];
if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled)
{
while (shrinkE < expandE && shrinkM % 10 == 0)
{
g.doDropDigit(shrinkM, shrinkE);
}
// We've got 128 bits of mantissa to work with here. Don't throw away data unless we
// have to
while (shrinkE < expandE && expandE > kMinExponent && expandM < kSafeLimit)
{
expandM *= 10;
--expandE;
}
}
while (shrinkE < expandE)
{
g.doDropDigit(shrinkM, shrinkE);
}
};
if (xe < ye)
{
if (xn)
g.setNegative();
do
{
g.doDropDigit(xm, xe);
} while (xe < ye);
adjust(ym, ye, xm, xe);
}
else if (xe > ye)
{
if (yn)
g.setNegative();
do
{
g.doDropDigit(ym, ye);
} while (xe > ye);
adjust(xm, xe, ym, ye);
}
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
auto const cuspRoundingFix = range.cuspRoundingFix;
if (xn == yn)
{
xm += ym;
if (xm > maxMantissa || xm > kMaxRep)
if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled)
{
g.doDropDigit(xm, xe);
while (xm > maxMantissa || xm > kMaxRepUp)
{
g.doDropDigit(xm, xe);
}
}
else
{
if (xm > maxMantissa || xm > kMaxRep)
{
g.doDropDigit(xm, xe);
}
}
g.doRoundUp(
xn,
xm,
xe,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
"Number::addition overflow");
xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, "Number::addition overflow");
}
else
{
@@ -779,6 +884,19 @@ Number::operator+=(Number const& y)
--xe;
}
g.doRoundDown(xn, xm, xe, minMantissa);
if (range.cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled && xm != 0)
{
// make a new guard
Guard g;
if (xn)
g.setNegative();
while (xm > maxMantissa || xm > kMaxRepUp)
{
g.doDropDigit(xm, xe);
}
g.doRoundUp(
xn, xm, xe, minMantissa, maxMantissa, cuspRoundingFix, "Number::addition overflow");
}
}
negative_ = xn;
@@ -825,9 +943,11 @@ Number::operator*=(Number const& y)
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
auto const cuspRoundingFix = range.cuspRoundingFix;
auto const kRepLimit =
cuspRoundingFix == MantissaRange::CuspRoundingFix::Disabled ? kMaxRep : kMaxRepUp;
while (zm > maxMantissa || zm > kMaxRep)
while (zm > maxMantissa || zm > kRepLimit)
{
g.doDropDigit(zm, ze);
}
@@ -840,7 +960,7 @@ Number::operator*=(Number const& y)
xe,
minMantissa,
maxMantissa,
cuspRoundingFixEnabled,
cuspRoundingFix,
"Number::multiplication overflow : exponent is " + std::to_string(xe));
negative_ = zn;
mantissa_ = xm;
@@ -882,7 +1002,7 @@ Number::operator/=(Number const& y)
auto const& range = kRange.get();
auto const& minMantissa = range.min;
auto const& maxMantissa = range.max;
auto const cuspRoundingFixEnabled = range.cuspRoundingFixEnabled;
auto const cuspRoundingFix = range.cuspRoundingFix;
// Division operates on two large integers (16-digit for small
// mantissas, 19-digit for large) using integer math. If the values
@@ -1014,14 +1134,14 @@ Number::operator/=(Number const& y)
// rounding fix is enabled, flag if there is still
// a remainder from stage 2.
bool const useTrailingRemainder =
cuspRoundingFixEnabled == MantissaRange::CuspRoundingFix::Enabled;
cuspRoundingFix != MantissaRange::CuspRoundingFix::Disabled;
if (useTrailingRemainder)
{
dropped = partialNumerator % dm != 0;
}
}
}
doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFixEnabled, dropped);
doNormalize(zp, zm, ze, minMantissa, maxMantissa, cuspRoundingFix, dropped);
negative_ = zp;
mantissa_ = static_cast<internalrep>(zm);
exponent_ = ze;
@@ -1033,6 +1153,8 @@ Number::operator/=(Number const& y)
Number::
operator rep() const
{
auto const& range = kRange.get();
rep drops = mantissa();
int offset = exponent();
Guard g;
@@ -1053,7 +1175,7 @@ operator rep() const
throw std::overflow_error("Number::operator rep() overflow");
drops *= 10;
}
g.doRound(drops, "Number::operator rep() rounding overflow");
g.doRound(drops, range.cuspRoundingFix, "Number::operator rep() rounding overflow");
}
return drops;
}

View File

@@ -43,6 +43,15 @@ class Number_test : public beast::unit_test::Suite
return out;
}
static BigInt
toBigInt(Number const& n)
{
BigInt v = n.mantissa();
for (int i = 0; i < n.exponent(); ++i)
v *= 10;
return v;
}
using dec = boost::multiprecision::cpp_dec_float_50;
template <class T = dec>
@@ -169,28 +178,37 @@ public:
auto const scale = Number::getMantissaScale();
testcase << "test_add " << to_string(scale);
using Case = std::tuple<Number, Number, Number>;
auto const cSmall = std::to_array<Case>(
{{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'066, -15}},
{Number{-1'000'000'000'000'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'066, -15}},
{Number{-1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{-9'999'999'999'999'344, -16}},
{Number{-6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'344, -16}},
{Number{}, Number{5}, Number{5}},
{Number{5}, Number{}, Number{5}},
{Number{5'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'554, -32768},
Number{0}},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16}}});
BEAST_EXPECT(Number::getround() == Number::RoundingMode::ToNearest);
using Case = std::tuple<Number, Number, Number, int>;
auto const cSmall = std::to_array<Case>({
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'066, -15},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'066, -15},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{-9'999'999'999'999'344, -16},
__LINE__},
{Number{-6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'344, -16},
__LINE__},
{Number{}, Number{5}, Number{5}, __LINE__},
{Number{5}, Number{}, Number{5}, __LINE__},
{Number{5'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'554, -32768},
Number{0},
__LINE__},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16},
__LINE__},
});
auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
@@ -198,45 +216,57 @@ public:
{
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'065'556, -18}},
Number{1'000'000'000'000'065'556, -18},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'065'556, -18}},
Number{-1'000'000'000'000'065'556, -18},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{-6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
{Number{}, Number{5}, Number{5}},
{Number{5}, Number{}, Number{5}},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{}, Number{5}, Number{5}, __LINE__},
{Number{5}, Number{}, Number{5}, __LINE__},
{Number{5'555'555'555'555'555'000, -32768},
Number{-5'555'555'555'555'554'000, -32768},
Number{0}},
Number{0},
__LINE__},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16}},
Number{9'999'999'999'999'990, -16},
__LINE__},
// Items from cSmall expanded for the larger mantissa
{Number{1'000'000'000'000'000'000, -18},
Number{6'555'555'555'555'555'555, -35},
Number{1'000'000'000'000'000'066, -18}},
Number{1'000'000'000'000'000'066, -18},
__LINE__},
{Number{-1'000'000'000'000'000'000, -18},
Number{-6'555'555'555'555'555'555, -35},
Number{-1'000'000'000'000'000'066, -18}},
Number{-1'000'000'000'000'000'066, -18},
__LINE__},
{Number{-1'000'000'000'000'000'000, -18},
Number{6'555'555'555'555'555'555, -35},
Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}},
Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}},
__LINE__},
{Number{-6'555'555'555'555'555'555, -35},
Number{1'000'000'000'000'000'000, -18},
Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}},
{Number{}, Number{5}, Number{5}},
Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}},
__LINE__},
{Number{}, Number{5}, Number{5}, __LINE__},
{Number{5'555'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'555'554, -32768},
Number{0}},
Number{0},
__LINE__},
{Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}},
Number{1'000'000'000'000'000'000, -18},
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}},
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}},
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}},
__LINE__},
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}, __LINE__},
// Test extremes
{
// Each Number operand rounds up, so the actual mantissa is
@@ -244,6 +274,7 @@ public:
Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}},
Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}},
Number{2, 19},
__LINE__,
},
{
// Does not round. Mantissas are going to be > kMaxRep, so if
@@ -254,21 +285,25 @@ public:
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}},
__LINE__,
},
});
auto const cLargeLegacy = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__},
});
auto const cLargeCorrected = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}},
{Number{Number::kMaxRep},
Number{6, -1},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
});
auto test = [this](auto const& c) {
for (auto const& [x, y, z] : c)
for (auto const& [x, y, z, line] : c)
{
auto const result = x + y;
std::stringstream ss;
ss << x << " + " << y << " = " << result << ". Expected: " << z;
BEAST_EXPECTS(result == z, ss.str());
expect(result == z, ss.str(), __FILE__, line);
}
};
if (scale == MantissaRange::MantissaScale::Small)
@@ -308,21 +343,28 @@ public:
auto const scale = Number::getMantissaScale();
testcase << "test_sub " << to_string(scale);
using Case = std::tuple<Number, Number, Number>;
using Case = std::tuple<Number, Number, Number, int>;
auto const cSmall = std::to_array<Case>(
{{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{9'999'999'999'999'344, -16}},
Number{9'999'999'999'999'344, -16},
__LINE__},
{Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{-9'999'999'999'999'344, -16}},
{Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}},
Number{-9'999'999'999'999'344, -16},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -15},
Number{0},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'001, -15},
Number{-1'000'000'000'000'000, -30}},
Number{-1'000'000'000'000'000, -30},
__LINE__},
{Number{1'000'000'000'000'001, -15},
Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -30}}});
Number{1'000'000'000'000'000, -30},
__LINE__}});
auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
@@ -330,49 +372,63 @@ public:
{
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
{Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -15},
Number{0},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'001, -15},
Number{-1'000'000'000'000'000, -30}},
Number{-1'000'000'000'000'000, -30},
__LINE__},
{Number{1'000'000'000'000'001, -15},
Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -30}},
Number{1'000'000'000'000'000, -30},
__LINE__},
// Items from cSmall expanded for the larger mantissa
{Number{1'000'000'000'000'000'000, -18},
Number{6'555'555'555'555'555'555, -32},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{6'555'555'555'555'555'555, -32},
Number{1'000'000'000'000'000'000, -18},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{1'000'000'000'000'000'000, -18},
Number{1'000'000'000'000'000'000, -18},
Number{0}},
Number{0},
__LINE__},
{Number{1'000'000'000'000'000'000, -18},
Number{1'000'000'000'000'000'001, -18},
Number{-1'000'000'000'000'000'000, -36}},
Number{-1'000'000'000'000'000'000, -36},
__LINE__},
{Number{1'000'000'000'000'000'001, -18},
Number{1'000'000'000'000'000'000, -18},
Number{1'000'000'000'000'000'000, -36}},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}},
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}},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{3, 0},
Number{Number::kMaxRep}},
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}},
Number{Number::kMaxRep},
__LINE__},
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__},
});
auto test = [this](auto const& c) {
for (auto const& [x, y, z] : c)
for (auto const& [x, y, z, line] : c)
{
auto const result = x - y;
std::stringstream ss;
ss << x << " - " << y << " = " << result << ". Expected: " << z;
BEAST_EXPECTS(result == z, ss.str());
expect(result == z, ss.str(), __FILE__, line);
}
};
if (scale == MantissaRange::MantissaScale::Small)
@@ -1644,9 +1700,7 @@ public:
BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue);
// What Number actually stored.
BigInt storedValue = BigInt(product.mantissa());
for (int i = 0; i < product.exponent(); ++i)
storedValue *= 10;
BigInt storedValue = toBigInt(product);
BigInt const signedDifference = storedValue - exactProduct;
@@ -1869,6 +1923,129 @@ public:
break;
}
}
{
testcase << "normalization cusp: ToNearest and Downward disagree " << to_string(scale);
constexpr auto kMaxRep = Number::kMaxRep;
// Both ToNearest and Downward should round to `below`
auto const actual = static_cast<std::uint64_t>(kMaxRep) + 1;
Number const below{static_cast<std::int64_t>(kMaxRep), 0};
Number const above{
false, static_cast<std::uint64_t>(kMaxRep) + 3, 0, Number::Unchecked{}};
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 << "\n"
<< " 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 rounds up
// 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;
case MantissaRange::MantissaScale::LargeLegacy:
// 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:
// 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);
}
}
{
testcase << "operator+ TowardsZero rounds away from zero " << to_string(scale);
Number const a{1LL, 20};
Number const b{-1'000'000'000'000'000'001LL};
BEAST_EXPECT(toBigInt(a) == BigInt{"100000000000000000000"});
if (scale != MantissaRange::MantissaScale::Small)
BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000001"});
else
BEAST_EXPECT(toBigInt(b) == BigInt{"-1000000000000000000"});
Number sum;
{
NumberRoundModeGuard const roundGuard{Number::RoundingMode::TowardsZero};
sum = a + b;
}
BigInt const exact = toBigInt(a) + toBigInt(b);
BigInt const stored = toBigInt(sum);
BigInt const diff = stored - exact;
log << "\n a = " << a << "\n b = " << b
<< "\n exact a + b = " << exact.str() << "\n TowardsZero = " << stored.str()
<< "\n difference = " << diff.str() << "\n";
log.flush();
if (scale == MantissaRange::MantissaScale::Small)
{
BEAST_EXPECT(stored == exact);
}
else if (scale == MantissaRange::MantissaScale::LargeLegacy)
{
BEAST_EXPECT(stored > exact);
}
else
{
BEAST_EXPECT(stored < exact);
BEAST_EXPECT(diff < 0);
BEAST_EXPECT(-diff < pow10<BigInt>(sum.exponent()));
}
}
}
void