mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-31 02:50:24 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user