mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-28 09:30:34 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user