From 0f2414b2a4b1b3babd07e0153569fa4fd30aabe2 Mon Sep 17 00:00:00 2001 From: Drew Paroski Date: Thu, 29 Apr 2021 00:01:01 +0000 Subject: SERVER-56326 Add a round() method to the Decimal128 class (cherry picked from commit cdfe92c4bcf1a4ff9f43cab56c587bca74ca2ffc) (cherry picked from commit 0c74ca79e7ab0385040f835ab7d3eb6d2d3063ee) (cherry picked from commit b5e1fb13cc2c67d48877e30bffae57a9c29b7b62) --- src/mongo/platform/decimal128.cpp | 26 ++++++ src/mongo/platform/decimal128.h | 7 ++ src/mongo/platform/decimal128_test.cpp | 165 +++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) diff --git a/src/mongo/platform/decimal128.cpp b/src/mongo/platform/decimal128.cpp index 73d315acd9b..7110d92d249 100644 --- a/src/mongo/platform/decimal128.cpp +++ b/src/mongo/platform/decimal128.cpp @@ -322,6 +322,32 @@ Decimal128 Decimal128::toAbs() const { return Decimal128(libraryTypeToValue(dec128)); } +Decimal128 Decimal128::round(RoundingMode roundMode) const { + std::uint32_t throwAwayFlag = 0; + return round(&throwAwayFlag, roundMode); +} + +Decimal128 Decimal128::round(std::uint32_t* signalingFlags, RoundingMode roundMode) const { + BID_UINT128 current = decimal128ToLibraryType(_value); + current = [&]() { + switch (roundMode) { + case kRoundTiesToEven: + return bid128_round_integral_nearest_even(current, signalingFlags); + case kRoundTowardNegative: + return bid128_round_integral_negative(current, signalingFlags); + case kRoundTowardPositive: + return bid128_round_integral_positive(current, signalingFlags); + case kRoundTowardZero: + return bid128_round_integral_zero(current, signalingFlags); + case kRoundTiesToAway: + return bid128_round_integral_nearest_away(current, signalingFlags); + } + MONGO_UNREACHABLE; + }(); + + return Decimal128{libraryTypeToValue(current)}; +} + std::int32_t Decimal128::toInt(RoundingMode roundMode) const { std::uint32_t throwAwayFlag = 0; return toInt(&throwAwayFlag, roundMode); diff --git a/src/mongo/platform/decimal128.h b/src/mongo/platform/decimal128.h index 5a1508317aa..6d4b42f31e8 100644 --- a/src/mongo/platform/decimal128.h +++ b/src/mongo/platform/decimal128.h @@ -249,6 +249,9 @@ public: return Decimal128(negated); } + Decimal128 round(RoundingMode roundMode = kRoundTiesToEven) const; + Decimal128 round(std::uint32_t* signalingFlags, + RoundingMode roundMode = kRoundTiesToEven) const; /** * This set of functions converts a Decimal128 to a certain integer type with a @@ -427,6 +430,10 @@ public: return _value.high64 == other._value.high64 && _value.low64 == other._value.low64; } + Decimal128 operator-() const { + return negate(); + } + private: static const uint8_t kSignFieldPos = 64 - 1; static const uint8_t kCombinationFieldPos = kSignFieldPos - 17; diff --git a/src/mongo/platform/decimal128_test.cpp b/src/mongo/platform/decimal128_test.cpp index bb38254d0ff..27d213ff9e4 100644 --- a/src/mongo/platform/decimal128_test.cpp +++ b/src/mongo/platform/decimal128_test.cpp @@ -1075,6 +1075,171 @@ TEST(Decimal128Test, TestDecimal128Quantize) { ASSERT_EQUALS(result.getValue().high64, expected.getValue().high64); } +struct RoundingModeInfo { + Decimal128::RoundingMode mode; + StringData name; +}; + +static constexpr std::array roundingModes{ + {{Decimal128::kRoundTiesToEven, "kRoundTiesToEven"_sd}, + {Decimal128::kRoundTowardNegative, "kRoundTowardNegative"_sd}, + {Decimal128::kRoundTowardPositive, "kRoundTowardPositive"_sd}, + {Decimal128::kRoundTowardZero, "kRoundTowardZero"_sd}, + {Decimal128::kRoundTiesToAway, "kRoundTiesToAway"_sd}}}; + +std::string convertUint64ToString(uint64_t value) { + constexpr int hexStrLength = sizeof(uint64_t) * 2; + char buffer[hexStrLength + 1]; + int n = snprintf(buffer, sizeof(buffer), "%016llX", static_cast(value)); + invariant(n >= 0); + return std::string(buffer); +} + +void assertRoundingTestCase(const Decimal128& actual, + const Decimal128& expected, + StringData roundingModeName, + int testCaseLineNumber) { + auto const& actualVal = actual.getValue(); + auto const& expectedVal = expected.getValue(); + + if (actualVal.low64 != expectedVal.low64 || actualVal.high64 != expectedVal.high64) { + FAIL(str::stream() << "Rounding test case defined on line " << testCaseLineNumber + << " failed. Rounding mode: " + << roundingModeName + << ". " + << "Expected: {" + << convertUint64ToString(expectedVal.low64) + << ", " + << convertUint64ToString(expectedVal.high64) + << "}. Actual: {" + << convertUint64ToString(actualVal.low64) + << ", " + << convertUint64ToString(actualVal.high64) + << "}"); + } +} + +TEST(Decimal128Test, TestDecimal128RoundingFractionalValues) { + auto d = [](auto x) { return Decimal128{x}; }; + + // 'pBig' is the largest positive integer value where Decimal128 can represent pBig + 0.1 + // without losing precision. + auto pBig = d(std::string(33, '9')); + auto nBig = -pBig; + + auto pBigPlus1 = pBig.add(d(1)); + auto nBigMinus1 = nBig.add(d(-1)); + + // 'epsilon' is the smallest positive value where Decimal128 can represent 1.0 + epsilon + // without losing precision. + auto epsilon = d("1E-33"); + + struct TestCase { + int lineNumber; + Decimal128 val; + Decimal128 expected[5]; + }; + + std::array cases = {{ + {__LINE__, d(0.5).add(d("-1E-34")), {d(0), d(0), d(1), d(0), d(0)}}, + {__LINE__, d(0.5), {d(0), d(0), d(1), d(0), d(1)}}, + {__LINE__, d(0.5).add(d("1E-34")), {d(1), d(0), d(1), d(0), d(1)}}, + {__LINE__, d(1.5).add(-epsilon), {d(1), d(1), d(2), d(1), d(1)}}, + {__LINE__, d(1.5), {d(2), d(1), d(2), d(1), d(2)}}, + {__LINE__, d(1.5).add(epsilon), {d(2), d(1), d(2), d(1), d(2)}}, + {__LINE__, d(2.5).add(-epsilon), {d(2), d(2), d(3), d(2), d(2)}}, + {__LINE__, d(2.5), {d(2), d(2), d(3), d(2), d(3)}}, + {__LINE__, d(2.5).add(epsilon), {d(3), d(2), d(3), d(2), d(3)}}, + {__LINE__, d(3.5).add(-epsilon), {d(3), d(3), d(4), d(3), d(3)}}, + {__LINE__, d(3.5), {d(4), d(3), d(4), d(3), d(4)}}, + {__LINE__, d(3.5).add(epsilon), {d(4), d(3), d(4), d(3), d(4)}}, + {__LINE__, d(9.5).add(-epsilon), {d(9), d(9), d(10), d(9), d(9)}}, + {__LINE__, d(9.5), {d(10), d(9), d(10), d(9), d(10)}}, + {__LINE__, d(9.5).add(epsilon), {d(10), d(9), d(10), d(9), d(10)}}, + {__LINE__, d(-0.5).add(d("1E-34")), {d(-0.0), d(-1), d(-0.0), d(-0.0), d(-0.0)}}, + {__LINE__, d(-0.5), {d(-0.0), d(-1), d(-0.0), d(-0.0), d(-1)}}, + {__LINE__, d(-0.5).add(d("-1E-34")), {d(-1), d(-1), d(-0.0), d(-0.0), d(-1)}}, + {__LINE__, d(-1.5).add(epsilon), {d(-1), d(-2), d(-1), d(-1), d(-1)}}, + {__LINE__, d(-1.5), {d(-2), d(-2), d(-1), d(-1), d(-2)}}, + {__LINE__, d(-1.5).add(-epsilon), {d(-2), d(-2), d(-1), d(-1), d(-2)}}, + {__LINE__, d(-2.5).add(epsilon), {d(-2), d(-3), d(-2), d(-2), d(-2)}}, + {__LINE__, d(-2.5), {d(-2), d(-3), d(-2), d(-2), d(-3)}}, + {__LINE__, d(-2.5).add(-epsilon), {d(-3), d(-3), d(-2), d(-2), d(-3)}}, + {__LINE__, d(-3.5).add(epsilon), {d(-3), d(-4), d(-3), d(-3), d(-3)}}, + {__LINE__, d(-3.5), {d(-4), d(-4), d(-3), d(-3), d(-4)}}, + {__LINE__, d(-3.5).add(-epsilon), {d(-4), d(-4), d(-3), d(-3), d(-4)}}, + {__LINE__, d(-9.5).add(epsilon), {d(-9), d(-10), d(-9), d(-9), d(-9)}}, + {__LINE__, d(-9.5), {d(-10), d(-10), d(-9), d(-9), d(-10)}}, + {__LINE__, d(-9.5).add(-epsilon), {d(-10), d(-10), d(-9), d(-9), d(-10)}}, + {__LINE__, pBig.add(d("0.4")), {pBig, pBig, pBigPlus1, pBig, pBig}}, + {__LINE__, pBig.add(d("0.5")), {pBigPlus1, pBig, pBigPlus1, pBig, pBigPlus1}}, + {__LINE__, pBig.add(d("0.6")), {pBigPlus1, pBig, pBigPlus1, pBig, pBigPlus1}}, + {__LINE__, nBig.add(d("-0.4")), {nBig, nBigMinus1, nBig, nBig, nBig}}, + {__LINE__, nBig.add(d("-0.5")), {nBigMinus1, nBigMinus1, nBig, nBig, nBigMinus1}}, + {__LINE__, nBig.add(d("-0.6")), {nBigMinus1, nBigMinus1, nBig, nBig, nBigMinus1}}, + }}; + + for (auto testCase : cases) { + for (size_t i = 0; i < roundingModes.size(); ++i) { + auto actual = testCase.val.round(roundingModes[i].mode); + assertRoundingTestCase( + actual, testCase.expected[i], roundingModes[i].name, testCase.lineNumber); + } + } +} + +TEST(Decimal128Test, TestDecimal128RoundingIntegerValues) { + struct TestCase { + int lineNumber; + Decimal128 val; + }; + + std::array cases = {{{__LINE__, Decimal128{"0"}}, + {__LINE__, Decimal128{"1"}}, + {__LINE__, Decimal128{"2"}}, + {__LINE__, Decimal128{"3"}}, + {__LINE__, Decimal128{"4"}}, + {__LINE__, Decimal128{DBL_MAX}}, + {__LINE__, Decimal128{std::string(34, '9')}}, + {__LINE__, Decimal128{"-0"}}, + {__LINE__, Decimal128{"-1"}}, + {__LINE__, Decimal128{"-2"}}, + {__LINE__, Decimal128{"-3"}}, + {__LINE__, Decimal128{"-4"}}, + {__LINE__, Decimal128{-DBL_MAX}}, + {__LINE__, Decimal128{"-" + std::string(34, '9')}}, + {__LINE__, Decimal128::kLargestPositive}, + {__LINE__, Decimal128::kLargestNegative}}}; + + for (auto testCase : cases) { + for (size_t i = 0; i < roundingModes.size(); ++i) { + auto actual = testCase.val.round(roundingModes[i].mode); + auto const& expected = testCase.val; + assertRoundingTestCase(actual, expected, roundingModes[i].name, testCase.lineNumber); + } + } +} + +TEST(Decimal128Test, TestDecimal128RoundingInfinityAndNan) { + struct TestCase { + int lineNumber; + Decimal128 val; + }; + + std::array cases = {{{__LINE__, Decimal128::kPositiveInfinity}, + {__LINE__, Decimal128::kNegativeInfinity}, + {__LINE__, Decimal128::kPositiveNaN}, + {__LINE__, Decimal128::kNegativeNaN}}}; + + for (auto testCase : cases) { + for (size_t i = 0; i < roundingModes.size(); ++i) { + auto actual = testCase.val.round(roundingModes[i].mode); + auto const& expected = testCase.val; + assertRoundingTestCase(actual, expected, roundingModes[i].name, testCase.lineNumber); + } + } +} + TEST(Decimal128Test, TestDecimal128NormalizeSmallVals) { Decimal128 d1("500E-2"); Decimal128 d2("5"); -- cgit v1.2.1