diff options
author | A. Jesse Jiryu Davis <jesse@mongodb.com> | 2019-05-23 17:15:39 -0400 |
---|---|---|
committer | A. Jesse Jiryu Davis <jesse@mongodb.com> | 2019-06-06 12:18:31 -0400 |
commit | 0d8e5d0e79431df3f25cd49914a16e2dd61d064c (patch) | |
tree | ac8716822914c420195886f9d152b373a36b3329 /src | |
parent | c27cafcd34190924cd62267fa8b42baf3caa69cb (diff) | |
download | mongo-0d8e5d0e79431df3f25cd49914a16e2dd61d064c.tar.gz |
SERVER-23077 Support Decimal128 with representAs().
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/platform/decimal128.cpp | 27 | ||||
-rw-r--r-- | src/mongo/platform/decimal128.h | 4 | ||||
-rw-r--r-- | src/mongo/util/represent_as.h | 134 | ||||
-rw-r--r-- | src/mongo/util/represent_as_test.cpp | 173 |
4 files changed, 297 insertions, 41 deletions
diff --git a/src/mongo/platform/decimal128.cpp b/src/mongo/platform/decimal128.cpp index 94581933375..18d59af7490 100644 --- a/src/mongo/platform/decimal128.cpp +++ b/src/mongo/platform/decimal128.cpp @@ -193,6 +193,9 @@ Decimal128::Decimal128(std::int32_t int32Value) Decimal128::Decimal128(std::int64_t int64Value) : _value(libraryTypeToValue(bid128_from_int64(int64Value))) {} +Decimal128::Decimal128(std::uint64_t uint64Value) + : _value(libraryTypeToValue(bid128_from_uint64(uint64Value))) {} + /** * Quantize a doubleValue argument to a Decimal128 with exactly 15 digits * of precision. @@ -527,6 +530,30 @@ std::int64_t Decimal128::toLongExact(std::uint32_t* signalingFlags, RoundingMode } } +std::uint64_t Decimal128::toULongExact(RoundingMode roundMode) const { + std::uint32_t throwAwayFlag = 0; + return toLongExact(&throwAwayFlag, roundMode); +} + +std::uint64_t Decimal128::toULongExact(std::uint32_t* signalingFlags, + RoundingMode roundMode) const { + BID_UINT128 dec128 = decimal128ToLibraryType(_value); + switch (roundMode) { + case kRoundTiesToEven: + return bid128_to_uint64_xrnint(dec128, signalingFlags); + case kRoundTowardNegative: + return bid128_to_uint64_xfloor(dec128, signalingFlags); + case kRoundTowardPositive: + return bid128_to_uint64_xceil(dec128, signalingFlags); + case kRoundTowardZero: + return bid128_to_uint64_xint(dec128, signalingFlags); + case kRoundTiesToAway: + return bid128_to_uint64_xrninta(dec128, signalingFlags); + default: + return bid128_to_uint64_xrnint(dec128, signalingFlags); + } +} + double Decimal128::toDouble(RoundingMode roundMode) const { std::uint32_t throwAwayFlag = 0; return toDouble(&throwAwayFlag, roundMode); diff --git a/src/mongo/platform/decimal128.h b/src/mongo/platform/decimal128.h index 90104354f37..7b96d8e2611 100644 --- a/src/mongo/platform/decimal128.h +++ b/src/mongo/platform/decimal128.h @@ -171,6 +171,7 @@ public: explicit Decimal128(std::int32_t int32Value); explicit Decimal128(std::int64_t int64Value); + explicit Decimal128(std::uint64_t uint64Value); /** * This constructor takes a double and constructs a Decimal128 object given a roundMode, either @@ -372,6 +373,9 @@ public: std::int64_t toLongExact(RoundingMode roundMode = kRoundTiesToEven) const; std::int64_t toLongExact(std::uint32_t* signalingFlags, RoundingMode roundMode = kRoundTiesToEven) const; + std::uint64_t toULongExact(RoundingMode roundMode = kRoundTiesToEven) const; + std::uint64_t toULongExact(std::uint32_t* signalingFlags, + RoundingMode roundMode = kRoundTiesToEven) const; /** * These functions convert decimals to doubles and have the ability to signal diff --git a/src/mongo/util/represent_as.h b/src/mongo/util/represent_as.h index e5d8f17829c..de96c5dd5ac 100644 --- a/src/mongo/util/represent_as.h +++ b/src/mongo/util/represent_as.h @@ -33,10 +33,12 @@ #include <limits> #include <type_traits> +#include <boost/numeric/conversion/cast.hpp> #include <boost/optional.hpp> #include "mongo/base/static_assert.h" #include "mongo/stdx/type_traits.h" +#include "mongo/util/if_constexpr.h" namespace mongo { @@ -166,6 +168,17 @@ int compare(T t, U u) { return signedCompare(upconvert(t), upconvert(u)); } +/** + * Return true if number can be converted to Output type without underflow or overflow. + */ +template <typename Output, typename Input> +bool inRange(Input i) { + const auto floor = std::numeric_limits<Output>::lowest(); + const auto ceiling = std::numeric_limits<Output>::max(); + + return detail::compare(i, floor) >= 0 && detail::compare(i, ceiling) <= 0; +} + } // namespace detail /** @@ -180,63 +193,102 @@ int compare(T t, U u) { * auto v3 = representAs<int>(10.3); // v3 is disengaged */ template <typename Output, typename Input> -boost::optional<Output> representAs(Input number) { - if (std::is_same<Input, Output>::value) { - return {static_cast<Output>(number)}; +boost::optional<Output> representAs(Input number) try { + IF_CONSTEXPR(std::is_same_v<Input, Output>) { + return number; } - - // If number is NaN and Output can also represent NaN, return NaN - // Note: We need to specifically handle NaN here because of the way - // detail::compare is implemented. - { - // We use ADL here to allow types, such as Decimal, to supply their - // own definitions of isnan(). If the Input type does not define a - // custom isnan(), then we fall back to using std::isnan(). - using std::isnan; - if (std::is_floating_point<Input>::value && isnan(number)) { - if (std::is_floating_point<Output>::value) { - return {static_cast<Output>(number)}; + else IF_CONSTEXPR(std::is_same_v<Decimal128, Output>) { + // Use Decimal128's ctor taking (u)int64_t or double, if it's safe to cast to one of those. + IF_CONSTEXPR(std::is_integral_v<Input>) { + IF_CONSTEXPR(std::is_signed_v<Input>) { + return Decimal128{boost::numeric_cast<int64_t>(number)}; + } + else { + return Decimal128{boost::numeric_cast<uint64_t>(number)}; } } + else IF_CONSTEXPR(std::is_floating_point_v<Input>) { + return Decimal128{boost::numeric_cast<double>(number)}; + } + else { + return {}; + } } + else { + // If number is NaN and Output can also represent NaN, return NaN + // Note: We need to specifically handle NaN here because of the way + // detail::compare is implemented. + if (std::is_floating_point_v<Input> && std::isnan(number)) { + if (std::is_floating_point_v<Output>) { + return {static_cast<Output>(number)}; + } + } - // If Output is integral and number is a non-integral floating point value, - // return a disengaged optional. - if (std::is_floating_point<Input>::value && std::is_integral<Output>::value) { - if (!(std::trunc(number) == number)) { + // If Output is integral and number is a non-integral floating point value, + // return a disengaged optional. + IF_CONSTEXPR(std::is_floating_point_v<Input> && std::is_integral_v<Output>) { + if (!(std::trunc(number) == number)) { + return {}; + } + } + + if (!detail::inRange<Output>(number)) { return {}; } - } - const auto floor = std::numeric_limits<Output>::lowest(); - const auto ceiling = std::numeric_limits<Output>::max(); + Output numberOut(number); - // If number is out-of-bounds for Output type, fail. - if ((detail::compare(number, floor) < 0) || (detail::compare(number, ceiling) > 0)) { - return {}; - } + // Some integers cannot be exactly represented as floating point numbers. + // To check, we cast back to the input type if we can, and compare. + IF_CONSTEXPR(std::is_integral_v<Input> && std::is_floating_point_v<Output>) { + if (!detail::inRange<Input>(numberOut) || static_cast<Input>(numberOut) != number) { + return {}; + } + } - // Our number is within bounds, safe to perform a static_cast. - auto numberOut = static_cast<Output>(number); + return numberOut; + } +} catch (const boost::bad_numeric_cast&) { + return {}; +} - // Some integers cannot be exactly represented as floating point numbers. - // To check, we cast back to the input type if we can, and compare. - if (std::is_integral<Input>::value && std::is_floating_point<Output>::value) { - const auto inputFloor = std::numeric_limits<Input>::lowest(); - const auto inputCeiling = std::numeric_limits<Input>::max(); +// Overload for converting from Decimal128. +template <typename Output> +boost::optional<Output> representAs(const Decimal128& number) try { + std::uint32_t flags = 0; + Output numberOut; - // If it is not safe to cast back to the Input type, fail. - if ((detail::compare(numberOut, inputFloor) < 0) || - (detail::compare(numberOut, inputCeiling) > 0)) { - return {}; + IF_CONSTEXPR(std::is_same_v<Output, Decimal128>) { + return number; + } + else IF_CONSTEXPR(std::is_floating_point_v<Output>) { + numberOut = boost::numeric_cast<Output>(number.toDouble(&flags)); + } + else IF_CONSTEXPR(std::is_integral_v<Output>) { + IF_CONSTEXPR(std::is_signed_v<Output>) { + numberOut = boost::numeric_cast<Output>(number.toLongExact(&flags)); } - - if (number != static_cast<Input>(numberOut)) { - return {}; + else { + numberOut = boost::numeric_cast<Output>(number.toULongExact(&flags)); } } + else { + // Unsupported type. + return {}; + } + + // Decimal128::toDouble/toLongExact failed. + if (flags & (Decimal128::kUnderflow | Decimal128::kOverflow | Decimal128::kInvalid)) { + return {}; + } + + if (std::is_integral<Output>() && flags & Decimal128::kInexact) { + return {}; + } - return {static_cast<Output>(numberOut)}; + return numberOut; +} catch (const boost::bad_numeric_cast&) { + return {}; } } // namespace mongo diff --git a/src/mongo/util/represent_as_test.cpp b/src/mongo/util/represent_as_test.cpp index 1c7538cb3a9..bab9ba3ca53 100644 --- a/src/mongo/util/represent_as_test.cpp +++ b/src/mongo/util/represent_as_test.cpp @@ -31,6 +31,7 @@ #include <cmath> #include <limits> +#include <type_traits> #include <boost/optional.hpp> @@ -246,6 +247,178 @@ TEST(RepresentAs, UnsignedIntToDouble) { ASSERT(!(representAs<double>(kUInt64Max))); } +template <typename Number> +void decimal128ToNumber() { + const auto floor = std::to_string(std::numeric_limits<Number>::lowest()); + const auto ceiling = std::to_string(std::numeric_limits<Number>::max()); + + ASSERT_EQ(*representAs<Number>(Decimal128(floor)), std::numeric_limits<Number>::lowest()); + ASSERT_EQ(*representAs<Number>(Decimal128(ceiling)), std::numeric_limits<Number>::max()); + + ASSERT_EQ(*representAs<Number>(Decimal128::kNormalizedZero), static_cast<Number>(0)); + ASSERT_EQ(*representAs<Number>(Decimal128("5")), static_cast<Number>(5)); + + ASSERT(!representAs<Number>(Decimal128::kLargestPositive)); + ASSERT(!representAs<Number>(Decimal128::kLargestNegative)); +} + +template <typename Float> +void decimal128ToFloatingPoint() { + decimal128ToNumber<Float>(); + ASSERT_EQ(*representAs<Float>(Decimal128("-5")), -5); + ASSERT_EQ(*representAs<Float>(Decimal128("5.5")), 5.5); + ASSERT_EQ(*representAs<Float>(Decimal128("-5.5")), -5.5); +} + +TEST(RepresentAs, Decimal128ToFloat) { + decimal128ToFloatingPoint<float>(); +} + +TEST(RepresentAs, Decimal128ToDouble) { + decimal128ToFloatingPoint<double>(); +} + +template <typename Integer> +void decimal128ToInteger() { + decimal128ToNumber<Integer>(); + ASSERT(!representAs<Integer>(Decimal128("5.5"))); + ASSERT(!representAs<Integer>(Decimal128("-5.5"))); + + ASSERT(!representAs<Integer>(Decimal128::kLargestPositive)); + ASSERT(!representAs<Integer>(Decimal128::kLargestNegative)); + ASSERT(!representAs<Integer>(Decimal128::kSmallestPositive)); + ASSERT(!representAs<Integer>(Decimal128::kSmallestNegative)); + + IF_CONSTEXPR(std::is_signed<Integer>()) { + ASSERT_EQ(*representAs<Integer>(Decimal128("-5")), -5); + } + else { + ASSERT(!representAs<Integer>(Decimal128("-5"))); + } +} + +TEST(RepresentAs, Decimal128ToInt8) { + decimal128ToInteger<int8_t>(); +} + +TEST(RepresentAs, Decimal128ToUInt8) { + decimal128ToInteger<uint8_t>(); +} + +TEST(RepresentAs, Decimal128ToInt16) { + decimal128ToInteger<int16_t>(); +} + +TEST(RepresentAs, Decimal128ToUInt16) { + decimal128ToInteger<uint16_t>(); +} + +TEST(RepresentAs, Decimal128ToInt32) { + decimal128ToInteger<int32_t>(); +} + +TEST(RepresentAs, Decimal128ToUInt32) { + decimal128ToInteger<uint32_t>(); +} + +TEST(RepresentAs, Decimal128ToInt64) { + decimal128ToInteger<int64_t>(); +} + +TEST(RepresentAs, Decimal128ToUInt64) { + decimal128ToInteger<uint64_t>(); +} + +TEST(RepresentAs, Decimal128ToDecimal128) { + Decimal128 decimals[] = {Decimal128::kNormalizedZero, + Decimal128::kLargestPositive, + Decimal128::kSmallestPositive, + Decimal128::kLargestNegative, + Decimal128::kSmallestNegative, + Decimal128(5), + Decimal128(5.5), + Decimal128(-5), + Decimal128(-5.5)}; + + for (const auto& d : decimals) { + ASSERT(representAs<Decimal128>(d)->isEqual(d)); + } +} + +template <typename Integer> +void integerToDecimal128() { + std::vector<Integer> v{ + Integer{5}, std::numeric_limits<Integer>::lowest(), std::numeric_limits<Integer>::max()}; + + IF_CONSTEXPR(std::is_signed_v<Integer>) { + v.emplace_back(-5); + } + + for (const auto& n : v) { + ASSERT(representAs<Decimal128>(n)->isEqual(Decimal128(std::to_string(n)))); + } +} + +TEST(RepresentAs, Int8ToDecimal128) { + integerToDecimal128<int8_t>(); +} + +TEST(RepresentAs, UInt8ToDecimal128) { + integerToDecimal128<uint8_t>(); +} + +TEST(RepresentAs, Int16ToDecimal128) { + integerToDecimal128<int16_t>(); +} + +TEST(RepresentAs, UInt16ToDecimal128) { + integerToDecimal128<uint16_t>(); +} + +TEST(RepresentAs, Int32ToDecimal128) { + integerToDecimal128<int32_t>(); +} + +TEST(RepresentAs, UInt32ToDecimal128) { + integerToDecimal128<uint32_t>(); +} + +TEST(RepresentAs, Int64ToDecimal128) { + integerToDecimal128<int64_t>(); +} + +TEST(RepresentAs, UInt64ToDecimal128) { + integerToDecimal128<uint64_t>(); +} + +template <typename Float> +void floatToDecimal128() { + Float x = 5.5; + ASSERT(representAs<Decimal128>(x)->isEqual(Decimal128(std::to_string(x)))); + ASSERT(representAs<Decimal128>(-x)->isEqual(Decimal128(std::to_string(-x)))); + + // Assert extreme numbers can be represented as Decimal128, and one basic property of each. + using limits = std::numeric_limits<Float>; + ASSERT(representAs<Decimal128>(limits::lowest())->isLess(Decimal128(0))); + ASSERT(representAs<Decimal128>(limits::min())->isGreater(Decimal128(0))); + ASSERT(representAs<Decimal128>(limits::denorm_min())->isGreater(Decimal128(0))); + ASSERT(representAs<Decimal128>(limits::max())->isGreater(Decimal128(0))); + ASSERT(representAs<Decimal128>(limits::quiet_NaN())->isNaN()); + ASSERT(representAs<Decimal128>(-limits::quiet_NaN())->isNaN()); + ASSERT(representAs<Decimal128>(limits::infinity())->isInfinite()); + ASSERT(representAs<Decimal128>(-limits::infinity())->isInfinite()); + ASSERT(representAs<Decimal128>(Float{0})->isEqual(Decimal128(0))); + ASSERT(representAs<Decimal128>(Float{-0})->isEqual(Decimal128(0))); +} + +TEST(RepresentAs, FloatToDecimal128) { + floatToDecimal128<float>(); +} + +TEST(RepresentAs, DoubleToDecimal128) { + floatToDecimal128<double>(); +} + TEST(RepresentAs, PlatformDependent) { // signed char ASSERT(*(representAs<int>(kCharMax)) == kCharMaxAsInt); |