From 2cbc3c139e0469c98e49d4fe9453de2d15f6f4e4 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 9 Jun 2026 13:46:56 -0400 Subject: [PATCH] fix: Fix Number comparison operator (#7406) --- include/xrpl/basics/Number.h | 35 ++++++----- src/test/basics/Number_test.cpp | 104 ++++++++++++++++++++++++++++++-- 2 files changed, 121 insertions(+), 18 deletions(-) diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h index 93bef82a8c..cee0c45355 100644 --- a/include/xrpl/basics/Number.h +++ b/include/xrpl/basics/Number.h @@ -408,33 +408,40 @@ public: } friend constexpr bool - operator<(Number const& x, Number const& y) noexcept + operator<(Number const& l, Number const& r) noexcept { + bool const lneg = l.negative_; + bool const rneg = r.negative_; + // If the two amounts have different signs (zero is treated as positive) // then the comparison is true iff the left is negative. - bool const lneg = x.negative_; - bool const rneg = y.negative_; - if (lneg != rneg) return lneg; - // Both have same sign and the left is zero: the right must be - // greater than 0. - if (x.mantissa_ == 0) - return y.mantissa_ > 0; + // Both have same sign and the left is zero: both must be non-negative. + // If the right is greater than 0, then it is larger, so the comparison is true. + if (l.mantissa_ == 0) + return r.mantissa_ > 0; - // Both have same sign, the right is zero and the left is non-zero. - if (y.mantissa_ == 0) + // Both have same sign, the right is zero and the left is non-zero, so the left must be + // positive, and thus is larger, so the comparison is false. + if (r.mantissa_ == 0) return false; // Both have the same sign, compare by exponents: - if (x.exponent_ > y.exponent_) + if (l.exponent_ > r.exponent_) return lneg; - if (x.exponent_ < y.exponent_) + if (l.exponent_ < r.exponent_) return !lneg; - // If equal exponents, compare mantissas - return x.mantissa_ < y.mantissa_; + // If equal signs and exponents, compare mantissas. + if (lneg) + { + // If negative, the operator is reversed. + return l.mantissa_ > r.mantissa_; + } + + return l.mantissa_ < r.mantissa_; } /** Return the sign of the amount */ diff --git a/src/test/basics/Number_test.cpp b/src/test/basics/Number_test.cpp index 81019970ad..f4bd1c9d66 100644 --- a/src/test/basics/Number_test.cpp +++ b/src/test/basics/Number_test.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,8 @@ #include #include #include +#include +#include namespace xrpl { @@ -1386,10 +1389,103 @@ public: testRelationals() { testcase << "test_relationals " << to_string(Number::getMantissaScale()); - BEAST_EXPECT(!(Number{100} < Number{10})); - BEAST_EXPECT(Number{100} > Number{10}); - BEAST_EXPECT(Number{100} >= Number{10}); - BEAST_EXPECT(!(Number{100} <= Number{10})); + + { + auto test = [this](auto const& nums) { + BEAST_EXPECT(std::ranges::is_sorted(nums)); + + for (auto iter1 = nums.begin(); iter1 != nums.end(); ++iter1) + { + auto iter2 = iter1; + for (++iter2; iter2 != nums.end(); ++iter2) + { + Number const& smaller = *iter1; + Number const& larger = *iter2; + std::stringstream ss; + ss << smaller << " < " << larger; + auto const str = ss.str(); + + // The ==/!= operators use a completely different code path than <, etc. + // This helps detect a breakage in one but not the other. It also helps + // verify that the values are being ordered correctly. + BEAST_EXPECTS(smaller != larger, str + " (!=)"); + BEAST_EXPECTS(!(smaller == larger), str + " (==)"); + + // true results using operator< and derived operators + BEAST_EXPECTS(smaller < larger, str + " (<)"); + BEAST_EXPECTS(larger > smaller, str + " (>)"); + BEAST_EXPECTS(larger >= smaller, str + " (>=)"); + BEAST_EXPECTS(smaller <= larger, str + " (<=)"); + + // false results using operator< and derived operators + BEAST_EXPECTS(!(larger < smaller), str + " (! <)"); + BEAST_EXPECTS(!(smaller > larger), str + " (! >)"); + BEAST_EXPECTS(!(smaller >= larger), str + " (! >=)"); + BEAST_EXPECTS(!(larger <= smaller), str + " (! <=)"); + } + } + }; + + auto const intNums = [this]() { + // Inequality test cases are built from a list of sorted integers + auto const values = + std::to_array({-100, -50, -20, -10, -1, 0, 1, 10, 20, 50, 100}); + // Check this list is sorted before converting it to Numbers. + // That way if any of the other tests fail, we know it's because of code and not the + // source data. + BEAST_EXPECT(std::ranges::is_sorted(values)); + + std::vector result; + result.reserve(values.size()); + for (auto const v : values) + result.emplace_back(v); + return result; + }(); + + auto const otherNums = std::to_array({ + Number{-5, 100}, + Number{-1, 100}, + Number{-7, -10}, + Number{-2, -10}, + Number{0}, + Number{2, -10}, + Number{7, -10}, + Number{1, 100}, + Number{5, 100}, + }); + + test(intNums); + test(otherNums); + } + + { + // Equality test cases are . Number will be compared against itself + using Case = std::pair; + auto const c = std::to_array({ + {700, __LINE__}, + {50, __LINE__}, + {1, __LINE__}, + {0, __LINE__}, + {-1, __LINE__}, + {-30, __LINE__}, + {-600, __LINE__}, + }); + for (auto const& [n, line] : c) + { + auto const str = to_string(n); + + // NOLINTBEGIN(misc-redundant-expression) Explicitly testing operators with + // equivalent values + expect(n == n, str + " ==", __FILE__, line); + expect(!(n != n), str + " !=", __FILE__, line); + + expect(!(n < n), str + " < ", __FILE__, line); + expect(!(n > n), str + " >", __FILE__, line); + expect(n >= n, str + " >=", __FILE__, line); + expect(n <= n, str + " <=", __FILE__, line); + // NOLINTEND(misc-redundant-expression) + } + } } void