fix: Handle rounding just above kMaxRep more accurately (#7389)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
This commit is contained in:
Ed Hennis
2026-07-14 14:47:41 -04:00
committed by GitHub
parent 0a4676d947
commit f10dd7b450
8 changed files with 946 additions and 203 deletions

View File

@@ -364,6 +364,8 @@ 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 = ((kMaxRep / 10) + 1) * 10;
static_assert(kMaxRepUp == 9'223'372'036'854'775'810ULL);
// May need to make unchecked private
struct Unchecked
@@ -591,6 +593,13 @@ public:
std::pair<T, int>
normalizeToRange() const;
// Safely convert rep (int64) mantissa to internalrep (uint64). If the rep
// is negative, returns the positive value. This takes a little extra work
// because converting std::numeric_limits<std::int64_t>::min() flirts with
// UB, and can vary across compilers.
static internalrep
externalToInternal(rep mantissa);
private:
static thread_local RoundingMode mode;
// The available ranges for mantissa
@@ -645,13 +654,6 @@ private:
// exponent could go out of range, so it will be checked.
[[nodiscard]] Number
shiftExponent(int exponentDelta) const;
// Safely convert rep (int64) mantissa to internalrep (uint64). If the rep
// is negative, returns the positive value. This takes a little extra work
// because converting std::numeric_limits<std::int64_t>::min() flirts with
// UB, and can vary across compilers.
static internalrep
externalToInternal(rep mantissa);
};
constexpr Number::Number(bool negative, internalrep mantissa, int exponent, Unchecked) noexcept

View File

@@ -277,6 +277,25 @@ public:
void
doDropDigit(T& mantissa, int& exponent) noexcept;
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
void
doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location);
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
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 <UnsignedMantissa T>
void
pushOverflow(T mantissa);
enum class Round {
// The result is exact. No rounding is needed. Only used if cuspRoundingFix is Enabled330 or
// higher.
@@ -289,37 +308,22 @@ public:
// 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 <UnsignedMantissa T>
void
doRoundUp(bool& negative, T& mantissa, int& exponent, std::string location);
// Modify the result to the correctly rounded value
template <UnsignedMantissa T>
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 <UnsignedMantissa T>
void
bringIntoRange(bool& negative, T& mantissa, int& exponent);
bringIntoRange(bool& negative, T& mantissa, int& exponent) const;
};
inline void
@@ -349,6 +353,7 @@ Number::Guard::isNegative() const noexcept
inline void
Number::Guard::doPush(unsigned d) noexcept
{
XRPL_ASSERT(d < 10, "xrpl::Number::Guard::doPush : valid digit");
xbit_ = xbit_ || ((digits_ & 0x0000'0000'0000'000F) != 0);
digits_ >>= 4;
digits_ |= (d & 0x0000'0000'0000'000FULL) << 60;
@@ -396,10 +401,69 @@ Number::Guard::doDropDigit<uint128_t>(uint128_t& mantissa, int& exponent) noexce
++exponent;
}
template <UnsignedMantissa T>
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;
}
}
// The second step scales the final digit of the updated 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 const digit = static_cast<unsigned>((diff * 10) / spread);
XRPL_ASSERT(
digit < 10u && 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
{
@@ -445,17 +509,23 @@ Number::Guard::round() const noexcept
template <UnsignedMantissa T>
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
if (mantissa < minMantissa)
// rounding.
if (mantissa < minMantissa &&
(cuspRoundingFix < MantissaRange::CuspRoundingFix::Enabled330 || mantissa != 0))
{
mantissa *= 10;
--exponent;
}
if (exponent < kMinExponent)
// mantissa should never be 0, but if it _is_ assert, but fall back to making the result kZero.
if (exponent < kMinExponent ||
(cuspRoundingFix >= MantissaRange::CuspRoundingFix::Enabled330 && mantissa == 0))
{
// Engineers: If you hit this assert, you probably did something wrong in the operation
// leading up to the rounding work.
XRPL_ASSERT(mantissa != 0, "xrpl::Number::Guard::bringIntoRange : valid mantissa");
static constexpr Number kZero = Number{};
negative = kZero.negative_;
@@ -468,7 +538,9 @@ template <UnsignedMantissa T>
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) {
@@ -485,18 +557,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
@@ -514,6 +597,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::overflow_error>(std::string(location));
@@ -521,8 +612,10 @@ Number::Guard::doRoundUp(bool& negative, T& mantissa, int& exponent, std::string
template <UnsignedMantissa T>
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)
{
@@ -557,6 +650,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))
{
@@ -573,6 +668,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;
}
@@ -622,7 +719,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;
@@ -672,17 +771,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");
@@ -814,6 +913,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.
@@ -898,7 +1000,7 @@ Number::operator+=(Number const& y)
}
else
{
if (xm > maxMantissa || xm > kMaxRep)
if (xm > maxMantissa || xm > repLimit)
{
g.doDropDigit(xm, xe);
}
@@ -942,7 +1044,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();
@@ -1016,8 +1118,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);
}
@@ -1282,8 +1386,11 @@ to_string(Number const& amount)
}
std::string ret = negative ? "-" : "";
ret.append(std::to_string(mantissa));
ret.append(1, 'e');
ret.append(std::to_string(exponent));
if (exponent != 0)
{
ret.append(1, 'e');
ret.append(std::to_string(exponent));
}
return ret;
}

View File

@@ -45,7 +45,7 @@ setCurrentTransactionRules(std::optional<Rules> r)
// amendments must also be added to useRulesGuards.
bool const enableLargeNumbers =
!r || (r->enabled(featureSingleAssetVault) || r->enabled(featureLendingProtocol));
// If enableLargeNumbers is true, then useRulesGuard must also return true.
// If enableLargeNumbers is true, then useRulesGuards must also return true.
// However, the reverse is not true. Other amendments can cause the rules guard to be used,
// even though large numbers are _not_ used.
XRPL_ASSERT(

View File

@@ -255,8 +255,47 @@ numberFromJson(SField const& field, json::Value const& value)
Throw<std::runtime_error>("not a number");
}
return STNumber{
field, Number{parts.negative, parts.mantissa, parts.exponent, Number::Normalized{}}};
Number const num{parts.negative, parts.mantissa, parts.exponent, Number::Normalized{}};
// Canonicalize "parts" and "num" with each other by getting rid of trailing 0s until either the
// exponents match, or there are no more 0s. If the two results don't match exactly, then the
// value has been rounded one way or another, and should not be used, because it may lead to an
// unexpected result. canonicalizeParts is not to be confused with Number::canonicalize, because
// they have completely different goals.
auto canonicalizeParts = [](NumberParts p, int otherExponent) {
if (p.mantissa == 0)
return NumberParts{};
while (p.exponent < otherExponent && p.mantissa % 10 == 0)
{
p.mantissa /= 10;
++p.exponent;
}
return p;
};
auto const numberMantissa = num.mantissa();
auto const numberExponent = num.exponent();
auto const canonicalParts = canonicalizeParts(parts, numberExponent);
auto const canonicalNum = canonicalizeParts(
NumberParts{
.mantissa = Number::externalToInternal(numberMantissa),
.exponent = numberExponent,
.negative = numberMantissa < 0,
},
canonicalParts.exponent);
if (canonicalParts.mantissa != canonicalNum.mantissa ||
canonicalParts.exponent != canonicalNum.exponent ||
canonicalParts.negative != canonicalNum.negative)
{
Throw<std::runtime_error>("number cannot be represented");
}
return STNumber{field, num};
}
} // namespace xrpl

View File

@@ -53,6 +53,7 @@
#include <array>
#include <cstdint>
#include <exception>
#include <functional>
#include <memory>
#include <optional>
@@ -1437,18 +1438,77 @@ class LoanBroker_test : public beast::unit_test::Suite
env(tx2, Ter(temINVALID));
}
env.setParseFailureExpected(true);
try
{
auto const dm = power(2, 63) - 1;
BEAST_EXPECTS(dm > kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
tx2[sfDebtMaximum] = "9223372036854775808";
env(tx2, Ter(temINVALID));
// should throw in parser
fail();
}
catch (std::exception const& e)
{
auto const dm = power(2, 63) - 3;
BEAST_EXPECTS(dm == kMaxMpTokenAmount, to_string(dm));
tx2[sfDebtMaximum] = dm;
env(tx2, Ter(tesSUCCESS));
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.DebtMaximum' has invalid data.");
}
env.setParseFailureExpected(false);
if (Number::getMantissaScale() >= MantissaRange::MantissaScale::Large330)
{
// 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) + Number{1, -1};
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
{
// 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));
}
}
{

View File

@@ -61,6 +61,7 @@
#include <chrono>
#include <cstdint>
#include <exception>
#include <functional>
#include <limits>
#include <memory>
@@ -5246,11 +5247,15 @@ class Vault_test : public beast::unit_test::Suite
auto const maxInt64 = std::to_string(std::numeric_limits<std::int64_t>::max());
BEAST_EXPECT(maxInt64 == "9223372036854775807");
// Naming things is hard
auto const maxInt64Plus1 = std::to_string(
static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) + 1);
BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808");
// Naming things is hard
auto const maxInt64Plus2 = std::to_string(
static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max()) + 2);
BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809");
auto const initialXRP = to_string(kInitialXrp);
BEAST_EXPECT(initialXRP == "100000000000000000");
@@ -5277,25 +5282,58 @@ class Vault_test : public beast::unit_test::Suite
env(tx);
env.close();
tx[sfAssetsMaximum] = maxInt64Plus1;
env(tx, Ter(tefEXCEPTION));
env.close();
// There are several parse failures expected in this function, so just disable it once.
env.setParseFailureExpected(true);
try
{
tx[sfAssetsMaximum] = maxInt64Plus1;
env(tx, Ter(tefEXCEPTION));
env.close();
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
try
{
tx[sfAssetsMaximum] = maxInt64Plus2;
env(tx, Ter(tefEXCEPTION));
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
// 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");
tx[sfAssetsMaximum] = decimalTest;
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
env(tx);
env.close();
try
{
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;
env(tx);
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
auto const vaultSle = env.le(newKeylet);
if (!BEAST_EXPECT(vaultSle))
return;
BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 9223372036854776);
BEAST_EXPECT(!vaultSle);
}
{
@@ -5329,25 +5367,41 @@ class Vault_test : public beast::unit_test::Suite
env(tx);
env.close();
tx[sfAssetsMaximum] = maxInt64Plus1;
env(tx, Ter(tefEXCEPTION));
env.close();
try
{
tx[sfAssetsMaximum] = maxInt64Plus2;
env(tx, Ter(tefEXCEPTION));
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
// 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");
tx[sfAssetsMaximum] = decimalTest;
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
env(tx);
env.close();
try
{
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;
env(tx);
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
auto const vaultSle = env.le(newKeylet);
if (!BEAST_EXPECT(vaultSle))
return;
BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == 922337203685477581);
BEAST_EXPECT(!vaultSle);
}
{
@@ -5374,9 +5428,22 @@ class Vault_test : public beast::unit_test::Suite
env(tx);
env.close();
tx[sfAssetsMaximum] = maxInt64Plus1;
env(tx);
env.close();
// Since several tests are expected to have parser failures, leave this flag set for the
// remainder of this function.
env.setParseFailureExpected(true);
try
{
tx[sfAssetsMaximum] = maxInt64Plus2;
env(tx);
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
tx[sfAssetsMaximum] = "1000000000000000e80";
env.close();
@@ -5386,22 +5453,27 @@ 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");
tx[sfAssetsMaximum] = decimalTest;
auto const newKeylet = keylet::vault(owner.id(), env.seq(owner));
env(tx);
env.close();
try
{
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;
env(tx);
// should throw in parser
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()) ==
"invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
}
auto const vaultSle = env.le(newKeylet);
if (!BEAST_EXPECT(vaultSle))
return;
BEAST_EXPECT(
(vaultSle->at(sfAssetsMaximum) ==
Number{9223372036854776, 2, Number::Normalized{}}));
BEAST_EXPECT(!vaultSle);
}
{
tx[sfAssetsMaximum] = "9223372036854775807e40"; // max int64 * 10^40

View File

@@ -9,6 +9,7 @@
#include <xrpl/protocol/Serializer.h>
#include <cstdint>
#include <exception>
#include <initializer_list>
#include <limits>
#include <stdexcept>
@@ -101,6 +102,39 @@ struct STNumber_test : public beast::unit_test::Suite
BEAST_EXPECT(numberFromJson(sfNumber, "-0.000e6") == STNumber(sfNumber, 0));
{
auto const parseNumber = [](std::string const& boundary) {
return numberFromJson(sfNumber, boundary);
};
auto const expectParseThrows = [this, &parseNumber](std::string const& boundary) {
try
{
parseNumber(boundary);
fail();
}
catch (std::exception const& e)
{
BEAST_EXPECT(std::string(e.what()) == "number cannot be represented");
}
};
// Small rejects this; large scales parse it as 9223372036854775800e-1.
auto constexpr positiveBoundary = "922337203685477580";
auto constexpr negativeBoundary = "-922337203685477580";
if (Number::getMantissaScale() == MantissaRange::MantissaScale::Small)
{
expectParseThrows(positiveBoundary);
expectParseThrows(negativeBoundary);
}
else
{
BEAST_EXPECT(
parseNumber(positiveBoundary) ==
STNumber(sfNumber, Number{922'337'203'685'477'580, 0}));
BEAST_EXPECT(
parseNumber(negativeBoundary) ==
STNumber(sfNumber, Number{-922'337'203'685'477'580, 0}));
}
NumberRoundModeGuard const mg(Number::RoundingMode::TowardsZero);
// maxint64 9,223,372,036,854,775,807
auto const maxInt = std::to_string(std::numeric_limits<std::int64_t>::max());
@@ -108,23 +142,19 @@ struct STNumber_test : public beast::unit_test::Suite
auto const minInt = std::to_string(std::numeric_limits<std::int64_t>::min());
if (Number::getMantissaScale() == MantissaRange::MantissaScale::Small)
{
BEAST_EXPECT(
numberFromJson(sfNumber, maxInt) ==
STNumber(sfNumber, Number{9'223'372'036'854'775, 3}));
BEAST_EXPECT(
numberFromJson(sfNumber, minInt) ==
STNumber(sfNumber, Number{-9'223'372'036'854'775, 3}));
// min/maxInt can't be exactly represented with the small mantissa, so they
// don't parse, and are expected to throw.
expectParseThrows(maxInt);
expectParseThrows(minInt);
}
else
{
// with large mantissas, maxint is fine
BEAST_EXPECT(
numberFromJson(sfNumber, maxInt) ==
parseNumber(maxInt) ==
STNumber(sfNumber, Number{9'223'372'036'854'775'807, 0}));
BEAST_EXPECT(
numberFromJson(sfNumber, minInt) ==
STNumber(
sfNumber,
Number{true, 9'223'372'036'854'775'808ULL, 0, Number::Normalized{}}));
// but minint's mantissa is > kMaxRep, and so rounds, and thus can't be parsed
expectParseThrows(minInt);
}
}

View File

@@ -182,6 +182,35 @@ TEST(NumberTest, limits)
caught = true;
}
EXPECT_TRUE(caught);
if (scale == MantissaRange::MantissaScale::Large330)
{
// Normalization with the other scales, including the older large mantissa scales, will
// overflow.
Number const bigNum{Number::kMaxRepUp, Number::kMaxExponent, Number::Normalized{}};
// The display of large exponents won't go above kMaxExponent
EXPECT_EQ(to_string(bigNum), "9223372036854775810e32768") << bigNum;
// Perhaps surprisingly, this is ok, because the exponent range is related to when the
// number is _normalized_, and for mantissas > kMaxRep, the accessors return values that
// are not normalized.
EXPECT_EQ(bigNum.mantissa(), 922337203685477581ULL) << bigNum.mantissa();
EXPECT_EQ(bigNum.exponent(), 32769) << bigNum.exponent();
}
else
{
try
{
Number{Number::kMaxRepUp, Number::kMaxExponent, Number::Normalized{}};
ADD_FAILURE();
}
catch (std::overflow_error const& e)
{
std::string const expected =
(scale == MantissaRange::MantissaScale::Small ? "Number::normalize 1"
: "Number::normalize 1.5");
EXPECT_EQ(e.what(), expected) << e.what();
}
}
}
}
@@ -193,7 +222,11 @@ TEST(NumberTest, add)
auto const scale = Number::getMantissaScale();
EXPECT_EQ(Number::getround(), Number::RoundingMode::ToNearest)
<< to_string(Number::getround());
using Case = std::tuple<Number, Number, Number, int>;
// TODO: Move these to the blocks where they're used
auto const cSmall = std::to_array<Case>({
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
@@ -304,12 +337,15 @@ TEST(NumberTest, add)
auto const cLargeLegacy = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__},
});
auto const cLargeCorrected = std::to_array<Case>({
auto const cLarge320 = std::to_array<Case>({
{Number{Number::kMaxRep},
Number{6, -1},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
});
auto const cLargeCorrected = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep}, __LINE__},
});
auto test = [](auto const& c) {
for (auto const& [x, y, z, line] : c)
{
@@ -330,9 +366,28 @@ TEST(NumberTest, add)
{
test(cLargeLegacy);
}
else if (scale == MantissaRange::MantissaScale::Large320)
{
test(cLarge320);
}
else
{
test(cLargeCorrected);
// This has to be created in this block, because normalization with the other
// scales, including the older large mantissa scales, will overflow.
Number const bigResult{
Number::kMaxRepUp, Number::kMaxExponent, Number::Normalized{}};
auto const cBigNums = std::to_array<Case>({
{
// Add 3 to the mantissa to avoid rounding
Number::max(),
Number{3, Number::kMaxExponent},
bigResult,
__LINE__,
},
});
test(cBigNums);
}
}
{
@@ -381,7 +436,7 @@ TEST(NumberTest, sub)
Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -30},
__LINE__}});
auto const cLarge = std::to_array<Case>(
auto const cLargeAll = std::to_array<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
// with larger mantissa
@@ -428,16 +483,55 @@ TEST(NumberTest, sub)
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<Case>({
// 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<Case>({
// 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 = [](auto const& c) {
for (auto const& [x, y, z, line] : c)
{
@@ -447,13 +541,23 @@ TEST(NumberTest, sub)
EXPECT_EQ(result, z) << ss.str() << " Line: " << line;
}
};
if (scale == MantissaRange::MantissaScale::Small)
switch (scale)
{
test(cSmall);
}
else
{
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:
ADD_FAILURE();
break;
}
}
}
@@ -1370,38 +1474,39 @@ TEST(NumberTest, to_string)
auto const scale = Number::getMantissaScale();
auto test = [](Number const& n, std::string const& expected) {
auto test = [](Number const& n, std::string const& expected, int line) {
auto const result = to_string(n);
std::stringstream ss;
ss << "to_string(" << result << "). Expected: " << expected;
EXPECT_EQ(result, expected) << ss.str();
EXPECT_EQ(result, expected) << ss.str() << " Line: " << 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__);
test(Number(-2, 11) - 1, "-200000000001", __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);
@@ -1409,61 +1514,131 @@ TEST(NumberTest, to_string)
EXPECT_EQ(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<std::int64_t>::max(), -3}, "9223372036854775");
test(
Number{std::numeric_limits<std::int64_t>::max(), -3},
"9223372036854775",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::max(), -3}),
"-9223372036854775");
"-9223372036854775",
__LINE__);
test(
Number{std::numeric_limits<std::int64_t>::min(), 0}, "-9223372036854775e3");
Number{std::numeric_limits<std::int64_t>::min(), 0},
"-9223372036854775e3",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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();
EXPECT_EQ((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<std::int64_t>::max(), 0}, "9223372036854775807");
Number{std::numeric_limits<std::int64_t>::max(), 0},
"9223372036854775807",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::min(), 0},
"-9223372036854775800");
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::min(), 0},
"-9223372036854775807",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::min(), 0},
"-9223372036854775800",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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 kMaxRepUp, it will be rounded down to kMaxRep
test(
Number{std::numeric_limits<std::int64_t>::max(), 0} + 1,
"9223372036854775807",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::max(), 0} + 1,
"9223372036854775810",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::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<std::int64_t>::max(), 0} + 1, "9223372036854775810");
Number{std::numeric_limits<std::int64_t>::max(), 0} + 2,
"9223372036854775810",
__LINE__);
test(
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
"-9223372036854775810");
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 2),
"-9223372036854775810",
__LINE__);
break;
}
}
@@ -1851,15 +2026,14 @@ TEST(NumberTest, upward_rounding_produces_value_not_below_exact_at_k_max_rep_cus
auto const message = [&] {
std::ostringstream os;
os << "\n"
<< " a = " << fmt(BigInt(kAValue)) << "\n"
os << " 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";
return os.str();
};
@@ -1939,15 +2113,14 @@ TEST(NumberTest, upward_division_returns_value_not_below_exact_on_large_scale)
auto const message = [&] {
std::ostringstream os;
os << "\n"
<< " a = " << kAValue << "\n"
os << " a = " << kAValue << "\n"
<< " b = " << kBValue << "\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";
return os.str();
};
@@ -1997,15 +2170,14 @@ TEST(NumberTest, downward_division_returns_value_not_above_exact_on_large_scale)
auto const message = [&] {
std::ostringstream os;
os << "\n"
<< " a = " << kAValue << "\n"
os << " a = " << kAValue << "\n"
<< " b = " << kBValue << "\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";
return os.str();
};
@@ -2065,15 +2237,14 @@ TEST(NumberTest, to_nearest_division_uses_dropped_digits_on_large_scale)
auto const message = [&] {
std::ostringstream os;
os << "\n"
<< " a = " << kAValue << "\n"
os << " a = " << kAValue << "\n"
<< " b = " << kBValue << "\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";
return os.str();
};
@@ -2169,14 +2340,13 @@ TEST(NumberTest, subtraction_rounding)
auto const message = [&](auto const& r, auto const& sum) {
std::ostringstream os;
os << "\n a = " << a << " (" << fmt(bigA)
<< ")\n b = " << b << " (" << fmt(bigB)
<< ")\n exact a + b = " << fmt(exact) << "\n";
os << " a = " << a << " (" << fmt(bigA) << ")\n b = " << b
<< " (" << fmt(bigB) << ")\n exact a + b = " << fmt(exact) << "\n";
auto const diff = sum.first - exact;
auto const rLabel = to_string(r);
os << std::string(15 - rLabel.length(), ' ') << rLabel << " = " << fmt(sum.first)
<< "\n difference = " << fmt(diff) << "\n";
<< "\n difference = " << fmt(diff) << "\n\n";
return os.str();
};
@@ -2228,6 +2398,93 @@ TEST(NumberTest, subtraction_rounding)
}
}
TEST(NumberTest, normalization_cusp_tonearest_and_downward)
{
for (auto const mantissaScale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg{mantissaScale};
NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest};
auto const scale = Number::getMantissaScale();
constexpr auto kMaxRep = Number::kMaxRep;
// Both ToNearest and Downward should round to `below`
auto constexpr 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::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);
auto message = [&] {
std::ostringstream log;
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";
return log.str();
};
switch (scale)
{
case MantissaRange::MantissaScale::Small:
// With the small mantissa, everything but Downward rounds UP, including the
// reference values, "above" and "below"
EXPECT_EQ(below, above) << message();
EXPECT_EQ(upward, above) << message();
EXPECT_EQ(toNearest, above) << message();
EXPECT_LT(downward, below) << message();
break;
case MantissaRange::MantissaScale::LargeLegacy:
case MantissaRange::MantissaScale::Large320:
// Upward round UP
EXPECT_EQ(upward, above) << message();
// ToNearest rounds UP when the DOWN neighbor is strictly closer
EXPECT_EQ(toNearest, above) << message();
EXPECT_GT(toNearest, below) << message();
// Downward undershoots: it returns a value below `below`
EXPECT_LT(downward, below) << message();
// Both should have given the same answer, but they differ
EXPECT_GT(toNearest, downward) << message();
break;
default:
// Covers "Large" and any newly added scales
// Upward round UP
EXPECT_EQ(upward, above) << message();
// ToNearest rounds to the strictly closer DOWN neighbor
EXPECT_NE(toNearest, above) << message();
EXPECT_EQ(toNearest, below) << message();
// Downward also rounds to `below`
EXPECT_EQ(downward, below) << message();
// ToNearest rounds to downward
EXPECT_EQ(toNearest, downward) << message();
break;
}
}
}
TEST(NumberTest, number_add_directed_sign_wrong)
{
for (auto const mantissaScale : MantissaRange::getAllScales())
@@ -2460,4 +2717,180 @@ TEST(NumberTest, number_add_to_nearest_picks_farther)
}
}
TEST(NumberTest, number_cusp_rounding_with_fractional_parts)
{
for (auto const mantissaScale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg{mantissaScale};
auto const scale = Number::getMantissaScale();
Number const below{static_cast<std::int64_t>(Number::kMaxRep), 0};
Number const above{false, Number::kMaxRepUp, 0, Number::Normalized{}};
auto header = [&] {
std::ostringstream log;
log << "Scale: " << to_string(mantissaScale) << ", Below: " << below
<< ", Above: " << above << "\n";
return log.str();
};
auto const zeroPointFour = Number(4, -1);
auto const zeroPointFive = Number(5, -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 twoPointFive = Number(25, -1);
auto const twoPointSix = Number(26, -1);
auto const operands = std::to_array<Number>({
zeroPointFour,
zeroPointFive,
zeroPointSix,
onePointFour,
onePointFive,
onePointSix,
twoPointFour,
twoPointFive,
twoPointSix,
});
auto const modes = std::to_array<Number::RoundingMode>({
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 = [&]() {
// Returns "above" by default. The checks here are for exceptions.
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 < zeroPointFive)
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 < zeroPointFive)
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;
auto message = [&] {
std::stringstream ss;
ss << header() << "kMaxRep + " << operand << " rounded " << to_string(mode)
<< " to " << actual << ". Expected: " << expectedValue;
return ss.str();
};
EXPECT_EQ(actual, expectedValue) << message();
}
}
// 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;
auto message = [&] {
std::stringstream ss;
ss << header() << "kMaxRepUp - " << operand << " rounded " << to_string(mode)
<< " to " << actual << ". Expected: " << expectedValue;
return ss.str();
};
EXPECT_EQ(actual, expectedValue) << message();
}
}
}
}
} // namespace xrpl