Files
rippled/src/libxrpl/protocol/STNumber.cpp
Ed Hennis 8306ac7710 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>
2026-07-11 00:18:31 +00:00

263 lines
6.9 KiB
C++

#include <xrpl/protocol/STNumber.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Rules.h> // IWYU pragma: keep
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STTakesAsset.h>
#include <xrpl/protocol/Serializer.h>
#include <boost/lexical_cast.hpp>
#include <boost/regex/v5/regbase.hpp>
#include <boost/regex/v5/regex.hpp>
#include <boost/regex/v5/regex_match.hpp>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <ostream>
#include <stdexcept>
#include <string>
#include <utility>
namespace xrpl {
STNumber::STNumber(SField const& field, Number const& value) : STTakesAsset(field), value_(value)
{
}
STNumber::STNumber(SerialIter& sit, SField const& field) : STTakesAsset(field)
{
// We must call these methods in separate statements
// to guarantee their order of execution.
auto mantissa = sit.geti64();
auto exponent = sit.geti32();
value_ = Number{mantissa, exponent};
}
SerializedTypeID
STNumber::getSType() const
{
return STI_NUMBER;
}
std::string
STNumber::getText() const
{
return to_string(value_);
}
void
STNumber::associateAsset(Asset const& a)
{
STTakesAsset::associateAsset(a);
XRPL_ASSERT_PARTS(
getFName().shouldMeta(SField::kSmdNeedsAsset),
"STNumber::associateAsset",
"field needs asset");
roundToAsset(a, value_);
}
void
STNumber::add(Serializer& s) const
{
XRPL_ASSERT(getFName().isBinary(), "xrpl::STNumber::add : field is binary");
XRPL_ASSERT(getFName().fieldType == getSType(), "xrpl::STNumber::add : field type match");
auto value = value_;
auto const mantissa = value.mantissa();
auto const exponent = value.exponent();
SField const& field = getFName();
if (field.shouldMeta(SField::kSmdNeedsAsset))
{
// asset is defined in the STTakesAsset base class
if (asset_)
{
// The number should be rounded to the asset's precision, but round
// it here if it has an asset assigned.
roundToAsset(*asset_, value);
XRPL_ASSERT_PARTS(value_ == value, "xrpl::STNumber::add", "value is already rounded");
}
else
{
// There are circumstances where an already-rounded Number is
// serialized without being touched by a transactor, and thus
// without an asset. We can't know if it's rounded, because it could
// represent _anything_, particularly when serializing user-provided
// Json. Regardless, the only time we should be serializing an
// STNumber is when the scale is large.
XRPL_ASSERT_PARTS(
Number::getMantissaScale() != MantissaRange::MantissaScale::Small,
"xrpl::STNumber::add",
"STNumber only used with large mantissa scale");
}
}
XRPL_ASSERT_PARTS(
mantissa <= std::numeric_limits<std::int64_t>::max() &&
mantissa >= std::numeric_limits<std::int64_t>::min(),
"xrpl::STNumber::add",
"mantissa in valid range");
s.add64(mantissa);
s.add32(exponent);
}
Number const&
STNumber::value() const
{
return value_;
}
void
STNumber::setValue(Number const& v)
{
value_ = v;
}
STBase*
STNumber::copy(std::size_t n, void* buf) const
{
return emplace(n, buf, *this);
}
STBase*
STNumber::move(std::size_t n, void* buf)
{
return emplace(n, buf, std::move(*this));
}
bool
STNumber::isEquivalent(STBase const& t) const
{
XRPL_ASSERT(
t.getSType() == this->getSType(), "xrpl::STNumber::isEquivalent : field type match");
auto const& v = dynamic_cast<STNumber const&>(t);
return value_ == v;
}
bool
STNumber::isDefault() const
{
return value_ == Number();
}
std::ostream&
operator<<(std::ostream& out, STNumber const& rhs)
{
return out << rhs.getText();
}
NumberParts
partsFromString(std::string const& number)
{
static boost::regex const kReNumber(
"^" // the beginning of the string
"([-+]?)" // (optional) + or - character
"(0|[1-9][0-9]*)" // a number (no leading zeroes, unless 0)
"(\\.([0-9]+))?" // (optional) period followed by any number
"([eE]([+-]?)([0-9]+))?" // (optional) E, optional + or -, any number
"$",
boost::regex_constants::optimize);
boost::smatch match;
if (!boost::regex_match(number, match, kReNumber))
Throw<std::runtime_error>("'" + number + "' is not a number");
// Match fields:
// 0 = whole input
// 1 = sign
// 2 = integer portion
// 3 = whole fraction (with '.')
// 4 = fraction (without '.')
// 5 = whole exponent (with 'e')
// 6 = exponent sign
// 7 = exponent number
bool const negative = (match[1].matched && (match[1] == "-"));
std::uint64_t mantissa = 0;
int exponent = 0;
if (!match[4].matched) // integer only
{
mantissa = boost::lexical_cast<std::uint64_t>(std::string(match[2]));
exponent = 0;
}
else
{
// integer and fraction
mantissa = boost::lexical_cast<std::uint64_t>(match[2] + match[4]);
exponent = -(match[4].length());
}
if (match[5].matched)
{
// we have an exponent
if (match[6].matched && (match[6] == "-"))
{
exponent -= boost::lexical_cast<int>(std::string(match[7]));
}
else
{
exponent += boost::lexical_cast<int>(std::string(match[7]));
}
}
return {.mantissa = mantissa, .exponent = exponent, .negative = negative};
}
STNumber
numberFromJson(SField const& field, json::Value const& value)
{
NumberParts parts;
if (value.isInt())
{
if (value.asInt() >= 0)
{
parts.mantissa = value.asInt();
}
else
{
parts.mantissa = value.asAbsUInt();
parts.negative = true;
}
}
else if (value.isUInt())
{
parts.mantissa = value.asUInt();
}
else if (value.isString())
{
parts = partsFromString(value.asString());
XRPL_ASSERT_PARTS(
!getCurrentTransactionRules(), "xrpld::numberFromJson", "Not in a Transactor context");
// Number mantissas are much bigger than the allowable parsed values, so
// it can't be out of range.
static_assert(
// NOLINTNEXTLINE(misc-redundant-expression)
std::numeric_limits<std::uint64_t>::max() >=
std::numeric_limits<decltype(parts.mantissa)>::max());
}
else
{
Throw<std::runtime_error>("not a number");
}
return STNumber{
field, Number{parts.negative, parts.mantissa, parts.exponent, Number::Normalized{}}};
}
} // namespace xrpl