fix: Improve Number addition/subtraction rounding (#7369)

Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ed Hennis
2026-07-10 20:18:31 -04:00
committed by GitHub
parent fd2cc6dcb3
commit 8306ac7710
6 changed files with 994 additions and 354 deletions

View File

@@ -48,6 +48,22 @@ fmt(BigInt const& value)
return out;
}
BigInt
toBigInt(Number const& n)
{
BigInt v = n.mantissa();
auto e = n.exponent();
for (; e > 0; --e)
v *= 10;
for (; e < 0; ++e)
{
EXPECT_EQ(v % 10, 0);
v /= 10;
}
return v;
}
template <class T = Dec>
static T
pow10(int n)
@@ -177,28 +193,35 @@ TEST(NumberTest, add)
auto const scale = Number::getMantissaScale();
using Case = std::tuple<Number, Number, Number>;
auto const cSmall = std::to_array<Case>(
{{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'066, -15}},
{Number{-1'000'000'000'000'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'066, -15}},
{Number{-1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{-9'999'999'999'999'344, -16}},
{Number{-6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'344, -16}},
{Number{}, Number{5}, Number{5}},
{Number{5}, Number{}, Number{5}},
{Number{5'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'554, -32768},
Number{0}},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16}}});
using Case = std::tuple<Number, Number, Number, int>;
auto const cSmall = std::to_array<Case>({
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'066, -15},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'066, -15},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{-9'999'999'999'999'344, -16},
__LINE__},
{Number{-6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'344, -16},
__LINE__},
{Number{}, Number{5}, Number{5}, __LINE__},
{Number{5}, Number{}, Number{5}, __LINE__},
{Number{5'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'554, -32768},
Number{0},
__LINE__},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16},
__LINE__},
});
auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
@@ -206,45 +229,57 @@ TEST(NumberTest, add)
{
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'065'556, -18}},
Number{1'000'000'000'000'065'556, -18},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{-6'555'555'555'555'555, -29},
Number{-1'000'000'000'000'065'556, -18}},
Number{-1'000'000'000'000'065'556, -18},
__LINE__},
{Number{-1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{-6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
{Number{}, Number{5}, Number{5}},
{Number{5}, Number{}, Number{5}},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{}, Number{5}, Number{5}, __LINE__},
{Number{5}, Number{}, Number{5}, __LINE__},
{Number{5'555'555'555'555'555'000, -32768},
Number{-5'555'555'555'555'554'000, -32768},
Number{0}},
Number{0},
__LINE__},
{Number{-9'999'999'999'999'999, -31},
Number{1'000'000'000'000'000, -15},
Number{9'999'999'999'999'990, -16}},
Number{9'999'999'999'999'990, -16},
__LINE__},
// Items from cSmall expanded for the larger mantissa
{Number{1'000'000'000'000'000'000, -18},
Number{6'555'555'555'555'555'555, -35},
Number{1'000'000'000'000'000'066, -18}},
Number{1'000'000'000'000'000'066, -18},
__LINE__},
{Number{-1'000'000'000'000'000'000, -18},
Number{-6'555'555'555'555'555'555, -35},
Number{-1'000'000'000'000'000'066, -18}},
Number{-1'000'000'000'000'000'066, -18},
__LINE__},
{Number{-1'000'000'000'000'000'000, -18},
Number{6'555'555'555'555'555'555, -35},
Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}},
Number{true, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}},
__LINE__},
{Number{-6'555'555'555'555'555'555, -35},
Number{1'000'000'000'000'000'000, -18},
Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}}},
{Number{}, Number{5}, Number{5}},
Number{false, 9'999'999'999'999'999'344ULL, -19, Number::Normalized{}},
__LINE__},
{Number{}, Number{5}, Number{5}, __LINE__},
{Number{5'555'555'555'555'555'555, -32768},
Number{-5'555'555'555'555'555'554, -32768},
Number{0}},
Number{0},
__LINE__},
{Number{true, 9'999'999'999'999'999'999ULL, -37, Number::Normalized{}},
Number{1'000'000'000'000'000'000, -18},
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}}},
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}},
Number{false, 9'999'999'999'999'999'990ULL, -19, Number::Normalized{}},
__LINE__},
{Number{Number::kMaxRep - 1}, Number{1, 0}, Number{Number::kMaxRep}, __LINE__},
// Test extremes
{
// Each Number operand rounds up, so the actual mantissa is
@@ -252,6 +287,7 @@ TEST(NumberTest, add)
Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}},
Number{false, 9'999'999'999'999'999'999ULL, 0, Number::Normalized{}},
Number{2, 19},
__LINE__,
},
{
// Does not round. Mantissas are going to be > maxRep, so if
@@ -262,21 +298,25 @@ TEST(NumberTest, add)
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
Number{false, 9'999'999'999'999'999'990ULL, 0, Number::Normalized{}},
Number{false, 1'999'999'999'999'999'998ULL, 1, Number::Normalized{}},
__LINE__,
},
});
auto const cLargeLegacy = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep / 10, 1}, __LINE__},
});
auto const cLargeCorrected = std::to_array<Case>({
{Number{Number::kMaxRep}, Number{6, -1}, Number{(Number::kMaxRep / 10) + 1, 1}},
{Number{Number::kMaxRep},
Number{6, -1},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
});
auto test = [](auto const& c) {
for (auto const& [x, y, z] : c)
for (auto const& [x, y, z, line] : c)
{
auto const result = x + y;
std::stringstream ss;
ss << x << " + " << y << " = " << result << ". Expected: " << z;
EXPECT_EQ(result, z) << ss.str();
EXPECT_EQ(result, z) << ss.str() << " Line: " << line;
}
};
if (scale == MantissaRange::MantissaScale::Small)
@@ -319,21 +359,28 @@ TEST(NumberTest, sub)
auto const scale = Number::getMantissaScale();
using Case = std::tuple<Number, Number, Number>;
using Case = std::tuple<Number, Number, Number, int>;
auto const cSmall = std::to_array<Case>(
{{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{9'999'999'999'999'344, -16}},
Number{9'999'999'999'999'344, -16},
__LINE__},
{Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{-9'999'999'999'999'344, -16}},
{Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}},
Number{-9'999'999'999'999'344, -16},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -15},
Number{0},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'001, -15},
Number{-1'000'000'000'000'000, -30}},
Number{-1'000'000'000'000'000, -30},
__LINE__},
{Number{1'000'000'000'000'001, -15},
Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -30}}});
Number{1'000'000'000'000'000, -30},
__LINE__}});
auto const cLarge = std::to_array<Case>(
// Note that items with extremely large mantissas need to be
// calculated, because otherwise they overflow uint64. Items from C
@@ -341,49 +388,63 @@ TEST(NumberTest, sub)
{
{Number{1'000'000'000'000'000, -15},
Number{6'555'555'555'555'555, -29},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{6'555'555'555'555'555, -29},
Number{1'000'000'000'000'000, -15},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
{Number{1'000'000'000'000'000, -15}, Number{1'000'000'000'000'000, -15}, Number{0}},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -15},
Number{0},
__LINE__},
{Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'001, -15},
Number{-1'000'000'000'000'000, -30}},
Number{-1'000'000'000'000'000, -30},
__LINE__},
{Number{1'000'000'000'000'001, -15},
Number{1'000'000'000'000'000, -15},
Number{1'000'000'000'000'000, -30}},
Number{1'000'000'000'000'000, -30},
__LINE__},
// Items from cSmall expanded for the larger mantissa
{Number{1'000'000'000'000'000'000, -18},
Number{6'555'555'555'555'555'555, -32},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{false, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{6'555'555'555'555'555'555, -32},
Number{1'000'000'000'000'000'000, -18},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}}},
Number{true, 9'999'999'999'999'344'444ULL, -19, Number::Normalized{}},
__LINE__},
{Number{1'000'000'000'000'000'000, -18},
Number{1'000'000'000'000'000'000, -18},
Number{0}},
Number{0},
__LINE__},
{Number{1'000'000'000'000'000'000, -18},
Number{1'000'000'000'000'000'001, -18},
Number{-1'000'000'000'000'000'000, -36}},
Number{-1'000'000'000'000'000'000, -36},
__LINE__},
{Number{1'000'000'000'000'000'001, -18},
Number{1'000'000'000'000'000'000, -18},
Number{1'000'000'000'000'000'000, -36}},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}},
Number{1'000'000'000'000'000'000, -36},
__LINE__},
{Number{Number::kMaxRep}, Number{6, -1}, Number{Number::kMaxRep - 1}, __LINE__},
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{1, 0},
Number{(Number::kMaxRep / 10) + 1, 1}},
Number{(Number::kMaxRep / 10) + 1, 1},
__LINE__},
{Number{false, Number::kMaxRep + 1, 0, Number::Normalized{}},
Number{3, 0},
Number{Number::kMaxRep}},
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}},
Number{Number::kMaxRep},
__LINE__},
{power(2, 63), Number{3, 0}, Number{Number::kMaxRep}, __LINE__},
});
auto test = [](auto const& c) {
for (auto const& [x, y, z] : c)
for (auto const& [x, y, z, line] : c)
{
auto const result = x - y;
std::stringstream ss;
ss << x << " - " << y << " = " << result << ". Expected: " << z;
EXPECT_EQ(result, z) << ss.str();
EXPECT_EQ(result, z) << ss.str() << " Line: " << line;
}
};
if (scale == MantissaRange::MantissaScale::Small)
@@ -1365,8 +1426,7 @@ TEST(NumberTest, to_string)
"9223372036854775e3");
}
break;
case MantissaRange::MantissaScale::LargeLegacy:
case MantissaRange::MantissaScale::Large:
default:
// Test the edges
// ((exponent < -(28)) || (exponent > -(8)))))
test(Number::min(), "1e-32750");
@@ -1405,8 +1465,6 @@ TEST(NumberTest, to_string)
-(Number{std::numeric_limits<std::int64_t>::max(), 0} + 1),
"-9223372036854775810");
break;
default:
EXPECT_TRUE(false);
}
}
}
@@ -1787,9 +1845,7 @@ TEST(NumberTest, upward_rounding_produces_value_not_below_exact_at_k_max_rep_cus
BigInt const exactProduct = BigInt(kAValue) * BigInt(kBValue);
// What Number actually stored.
BigInt storedValue = BigInt(product.mantissa());
for (int i = 0; i < product.exponent(); ++i)
storedValue *= 10;
BigInt const storedValue = toBigInt(product);
BigInt const signedDifference = storedValue - exactProduct;
@@ -1809,7 +1865,8 @@ TEST(NumberTest, upward_rounding_produces_value_not_below_exact_at_k_max_rep_cus
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
EXPECT_TRUE(signedDifference >= 0) << message();
EXPECT_TRUE(signedDifference < pow10<BigInt>(product.exponent())) << message();
EXPECT_EQ(product.mantissa(), (std::numeric_limits<std::int64_t>::max() / 10) + 1);
@@ -1897,7 +1954,8 @@ TEST(NumberTest, upward_division_returns_value_not_below_exact_on_large_scale)
// Upward invariant: stored >= exact. Bug: stored < exact.
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
EXPECT_TRUE(stored >= exact) << message();
EXPECT_TRUE(diff < pow10(quotient.exponent())) << message();
break;
@@ -1951,10 +2009,11 @@ TEST(NumberTest, downward_division_returns_value_not_above_exact_on_large_scale)
return os.str();
};
// Downward invariant: stored <= exact. Bug: stored > exact.
// invariant: stored <= exact. Bug: stored > exact.
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
EXPECT_TRUE(stored <= exact) << message();
EXPECT_TRUE(diff > -pow10(quotient.exponent())) << message();
break;
@@ -2018,10 +2077,11 @@ TEST(NumberTest, to_nearest_division_uses_dropped_digits_on_large_scale)
return os.str();
};
// ToNearest should account for dropped digits beyond the visible "5".
// invariant: stored >= exact. Bug: stored < exact.
switch (scale)
{
case MantissaRange::MantissaScale::Large:
case MantissaRange::MantissaScale::Large320:
case MantissaRange::MantissaScale::Large330:
EXPECT_TRUE(stored >= exact) << message();
EXPECT_TRUE(diff < pow10(quotient.exponent())) << message();
break;
@@ -2039,4 +2099,365 @@ TEST(NumberTest, to_nearest_division_uses_dropped_digits_on_large_scale)
}
}
TEST(NumberTest, subtraction_rounding)
{
for (auto const mantissaScale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg{mantissaScale};
NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest};
auto const scale = Number::getMantissaScale();
auto const exp = Number::mantissaLog();
// SubCase is <offset, extraB, aString, bString>
// * offset: offset from exp
// * extraB: whether to include 1e"exp" in "b"
// * aString: expected string value for "a"
// * bString: expected string value for "b"
// There aren't too many valid combinations for test cases here. If extraB is true,
// offset can really only be 2, because any larger and the mantissa can't be represented
// without loss. Offset can't be less than 2, or there's no error.
using SubCase = std::tuple<int, bool, std::string, std::string>;
auto const c = std::to_array<SubCase>({
{2,
true,
scale == MantissaRange::MantissaScale::Small ? "100000000000000000"
: "100000000000000000000",
scale == MantissaRange::MantissaScale::Small ? "-1000000000000001"
: "-1000000000000000001"},
{2,
false,
scale == MantissaRange::MantissaScale::Small ? "100000000000000000"
: "100000000000000000000",
"-1"},
{30,
false,
scale == MantissaRange::MantissaScale::Small
? "1000000000000000000000000000000000000000000000"
: "1000000000000000000000000000000000000000000000000",
"-1"},
});
for (auto const& [offset, extraB, aString, bString] : c)
{
Number const a{1LL, exp + offset};
Number const b{-((extraB ? Number{1, exp} : kNumZero) + 1)};
auto const bigA = toBigInt(a);
auto const bigB = toBigInt(b);
EXPECT_EQ(bigA, BigInt{aString});
EXPECT_EQ(bigB, BigInt{bString});
auto construct = [&a, &b](Number::RoundingMode r) {
NumberRoundModeGuard const roundGuard{r};
auto const sum = a + b;
BigInt const stored = toBigInt(sum);
return std::make_pair(r, std::make_pair(stored, sum));
};
BigInt const exact = bigA + bigB;
auto const sums = [&]() {
std::map<Number::RoundingMode, std::pair<BigInt, Number>> r;
r.emplace(construct(Number::RoundingMode::TowardsZero));
r.emplace(construct(Number::RoundingMode::Upward));
r.emplace(construct(Number::RoundingMode::Downward));
r.emplace(construct(Number::RoundingMode::ToNearest));
return r;
}();
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";
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";
return os.str();
};
auto const expectedExponent =
offset - (scale == MantissaRange::MantissaScale::Small && extraB ? 1 : 0);
auto const epsilon = pow10<BigInt>(expectedExponent);
for (auto const& [r, sum] : sums)
{
auto diff = sum.first - exact;
switch (scale)
{
case MantissaRange::MantissaScale::Small:
case MantissaRange::MantissaScale::LargeLegacy:
case MantissaRange::MantissaScale::Large320: {
// Without the fix, all the results but one round up
if (r == Number::RoundingMode::Downward)
{
// Downward works because the Guard sign is negative, and Downward
// returns Up instead of Down if negative and there's a remainder,
// whereas TowardsZero always returns Down.
EXPECT_LT(sum.first, exact) << message(r, sum);
EXPECT_EQ(diff, -(epsilon - 1)) << message(r, sum);
}
else
{
EXPECT_GT(sum.first, exact) << message(r, sum);
EXPECT_EQ(diff, 1) << message(r, sum);
}
break;
}
default: {
EXPECT_LE(sum.second.exponent(), expectedExponent) << message(r, sum);
switch (r)
{
case Number::RoundingMode::Upward:
case Number::RoundingMode::ToNearest:
EXPECT_GT(sum.first, exact) << message(r, sum);
EXPECT_EQ(diff, 1) << message(r, sum);
break;
default:
EXPECT_LT(sum.first, exact) << message(r, sum);
EXPECT_EQ(diff, -(epsilon - 1)) << message(r, sum);
}
}
}
}
}
}
}
TEST(NumberTest, number_add_directed_sign_wrong)
{
for (auto const mantissaScale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg{mantissaScale};
NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest};
auto const scale = Number::getMantissaScale();
{
// Two negative numbers with the same exponent
Number const a{-6, Number::mantissaLog()};
Number const b{a - 3};
EXPECT_TRUE(a.exponent() == b.exponent() && abs(b) > abs(a));
BigInt const exact = toBigInt(a) + toBigInt(b);
if (scale == MantissaRange::MantissaScale::Small)
{
EXPECT_EQ(exact, BigInt{"-12000000000000003"});
}
else
{
EXPECT_EQ(exact, BigInt{"-12000000000000000003"});
}
Number down, up;
{
NumberRoundModeGuard const g{Number::RoundingMode::Downward};
down = a + b;
}
{
NumberRoundModeGuard const g{Number::RoundingMode::Upward};
up = a + b;
}
auto const valueDown = toBigInt(down);
auto const valueUp = toBigInt(up);
auto message = [&] {
std::ostringstream log;
log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown)
<< " (correct rounding: <= exact)"
<< "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n";
return log.str();
};
if (scale == MantissaRange::MantissaScale::Large330)
{
EXPECT_LE(valueDown, exact) << message(); // Downward should round away from zero
EXPECT_GE(valueUp, exact) << message(); // Upward should round toward 0
}
else
{
EXPECT_GT(valueDown, exact)
<< message(); // Downward rounded toward zero (too high)
EXPECT_LT(valueUp, exact) << message(); // Upward rounded toward -inf (too low)
}
}
{
// Positive control: the same magnitudes with a positive result round
Number const pa{6, Number::mantissaLog()};
Number const pb{pa + 3};
EXPECT_TRUE(pa.exponent() == pb.exponent() && abs(pb) > abs(pa));
BigInt const pexact = toBigInt(pa) + toBigInt(pb); // 12'000'000'000'000'000'003
Number pdown, pup;
{
NumberRoundModeGuard const g{Number::RoundingMode::Downward};
pdown = pa + pb;
}
{
NumberRoundModeGuard const g{Number::RoundingMode::Upward};
pup = pa + pb;
}
auto const valuePDown = toBigInt(pdown);
auto const valuePUp = toBigInt(pup);
auto message = [&] {
std::ostringstream log;
log << " exact = " << fmt(pexact) << "\n downward = " << fmt(valuePDown)
<< " (correct rounding: <= exact)"
<< "\n upward = " << fmt(valuePUp)
<< " (correct rounding: >= exact)\n\n";
return log.str();
};
EXPECT_LE(valuePDown, pexact) << message(); // correct for positive results
EXPECT_GE(valuePUp, pexact) << message();
}
{
// Mixed sign numbers with the same exponent: negative second value
Number const a{1, Number::mantissaLog()};
Number const b{Number{-9, Number::mantissaLog()} - 3};
EXPECT_TRUE(a.exponent() == b.exponent() && abs(b) > abs(a));
BigInt const exact = toBigInt(a) + toBigInt(b);
if (scale == MantissaRange::MantissaScale::Small)
{
EXPECT_EQ(exact, BigInt{"-8000000000000003"});
}
else
{
EXPECT_EQ(exact, BigInt{"-8000000000000000003"});
}
Number down, up;
{
NumberRoundModeGuard const g{Number::RoundingMode::Downward};
down = a + b;
}
{
NumberRoundModeGuard const g{Number::RoundingMode::Upward};
up = a + b;
}
auto const valueDown = toBigInt(down);
auto const valueUp = toBigInt(up);
auto message = [&] {
std::ostringstream log;
log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown)
<< " (correct rounding: <= exact)"
<< "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n";
return log.str();
};
EXPECT_LE(valueDown, exact) << message(); // Downward should round away from zero
EXPECT_GE(valueUp, exact) << message(); // Upward should round toward 0
}
{
// Mixed sign numbers with the same exponent: negative first value
Number const a{-1, Number::mantissaLog()};
Number const b{Number{9, Number::mantissaLog()} + 3};
EXPECT_TRUE(a.exponent() == b.exponent() && abs(b) > abs(a));
BigInt const exact = toBigInt(a) + toBigInt(b);
if (scale == MantissaRange::MantissaScale::Small)
{
EXPECT_EQ(exact, BigInt{"8000000000000003"});
}
else
{
EXPECT_EQ(exact, BigInt{"8000000000000000003"});
}
Number down, up;
{
NumberRoundModeGuard const g{Number::RoundingMode::Downward};
down = a + b;
}
{
NumberRoundModeGuard const g{Number::RoundingMode::Upward};
up = a + b;
}
auto const valueDown = toBigInt(down);
auto const valueUp = toBigInt(up);
auto message = [&] {
std::ostringstream log;
log << " exact = " << fmt(exact) << "\n downward = " << fmt(valueDown)
<< " (correct rounding: <= exact)"
<< "\n upward = " << fmt(valueUp) << " (correct rounding: >= exact)\n\n";
return log.str();
};
EXPECT_LE(valueDown, exact) << message(); // Downward should round away from zero
EXPECT_GE(valueUp, exact) << message(); // Upward should round toward 0
}
}
}
TEST(NumberTest, number_add_to_nearest_picks_farther)
{
for (auto const mantissaScale : MantissaRange::getAllScales())
{
NumberMantissaScaleGuard const mg{mantissaScale};
NumberRoundModeGuard const rg{Number::RoundingMode::ToNearest};
auto const scale = Number::getMantissaScale();
// Case is <y, expected q>
using Case = std::pair<Number, std::int64_t>;
auto const c = std::to_array<Case>({
{Number{5'175'909'259'972'499'745LL, 22}, -1'074'951'375'311'646'003},
{Number{1}, -1'074'956'551'220'905'975},
{Number{1, 10}, -1'074'956'551'220'905'975},
{Number{1, 20}, -1'074'956'551'220'905'975},
{Number{1, 27}, -1'074'956'551'220'905'975},
{Number{1, 28}, -1'074'956'551'220'905'974},
{Number{1, 31}, -1'074'956'551'220'904'975},
});
for (auto const& [y, expectedQ] : c)
{
Number const x{-1'074'956'551'220'905'975LL, 28};
Number const res = x + y;
BigInt const exact = toBigInt(x) + toBigInt(y);
BigInt const vres = toBigInt(res);
BigInt ulp = 1;
for (int i = 0; i < res.exponent(); ++i)
ulp *= 10;
BigInt const q = (exact - ulp / 2) / ulp;
Number const normalizedExact{static_cast<std::int64_t>(q), res.exponent()};
BigInt const norm = toBigInt(normalizedExact);
auto message = [&](auto const& comp) {
std::ostringstream log;
log << fmt(q) + " != " + fmt(comp) << "\n"
<< " x = " << x << "\n y = " << y
<< "\n exact = " << fmt(exact)
<< "\n result (x + y) = " << fmt(vres)
<< "\n normalize(exact) = " << fmt(norm) << "\n\n";
return log.str();
};
if (scale == MantissaRange::MantissaScale::Small)
{
auto const comp = toBigInt(Number{expectedQ, -3});
EXPECT_EQ(q, comp) << message(comp);
}
else
{
EXPECT_EQ(q, expectedQ) << message(BigInt(expectedQ));
}
EXPECT_EQ(normalizedExact, res);
}
}
}
} // namespace xrpl