diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc index 3b5aac4a8df..ea06eb5a365 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc @@ -503,6 +503,34 @@ struct CastFunctor::value>> { } }; +// ---------------------------------------------------------------------- +// Decimal to real + +struct DecimalToReal { + template + RealType Call(KernelContext* ctx, const Decimal128& val) const { + return val.ToReal(in_scale_); + } + + int32_t in_scale_; +}; + +template +struct CastFunctor::value>> { + static void Exec(KernelContext* ctx, const ExecBatch& batch, Datum* out) { + const auto& in_type_inst = + checked_cast(*batch[0].array()->type); + const auto in_scale = in_type_inst.scale(); + + applicator::ScalarUnaryNotNullStateful kernel( + DecimalToReal{in_scale}); + return kernel.Exec(ctx, batch, out); + } +}; + +// ---------------------------------------------------------------------- +// Top-level kernel instantiation + namespace { template @@ -558,8 +586,12 @@ std::shared_ptr GetCastToFloating(std::string name) { DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, out_ty, CastFloatingToFloating)); } - // From other numbers to integer + // From other numbers to floating point AddCommonNumberCasts(out_ty, func.get()); + + // From decimal to floating point + DCHECK_OK(func->AddKernel(Type::DECIMAL, {InputType::Array(Type::DECIMAL)}, out_ty, + CastFunctor::Exec)); return func; } diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 252e50ee231..7384a67e2e8 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -399,6 +399,14 @@ class TestCast : public TestBase { R"(["0.00", null, "0.00", "123.45", "999.99"])", /*check_scalar=*/true, options); } + + void TestCastDecimalToFloating(const std::shared_ptr& out_type) { + auto in_type = decimal(5, 2); + + CheckCaseJSON(in_type, out_type, R"(["0.00", null, "123.45", "999.99"])", + "[0.0, null, 123.45, 999.99]"); + // Edge cases are tested in Decimal128::ToReal() + } }; TEST_F(TestCast, SameTypeZeroCopy) { @@ -943,6 +951,8 @@ TEST_F(TestCast, FloatToDecimal) { out_type = decimal(20, 4); CheckCaseJSON(in_type, out_type, "[1.8446746e+15, -1.8446746e+15]", R"(["1844674627273280.7168", "-1844674627273280.7168"])"); + + // More edge cases tested in Decimal128::FromReal } TEST_F(TestCast, DoubleToDecimal) { @@ -957,6 +967,18 @@ TEST_F(TestCast, DoubleToDecimal) { out_type = decimal(20, 4); CheckCaseJSON(in_type, out_type, "[1.8446744073709556e+15, -1.8446744073709556e+15]", R"(["1844674407370955.5712", "-1844674407370955.5712"])"); + + // More edge cases tested in Decimal128::FromReal +} + +TEST_F(TestCast, DecimalToFloat) { + auto out_type = float32(); + TestCastDecimalToFloating(out_type); +} + +TEST_F(TestCast, DecimalToDouble) { + auto out_type = float64(); + TestCastDecimalToFloating(out_type); } TEST_F(TestCast, TimestampToTimestamp) { diff --git a/cpp/src/arrow/util/decimal.cc b/cpp/src/arrow/util/decimal.cc index 562080323de..20331ba1140 100644 --- a/cpp/src/arrow/util/decimal.cc +++ b/cpp/src/arrow/util/decimal.cc @@ -98,7 +98,7 @@ static constexpr double kDoublePowersOfTen[2 * 38 + 1] = { namespace { template -struct Decimal128FromReal { +struct DecimalRealConversion { static Result FromPositiveReal(Real real, int32_t precision, int32_t scale) { auto x = real; @@ -140,24 +140,59 @@ struct Decimal128FromReal { return FromPositiveReal(x, precision, scale); } } + + static Real ToRealPositive(const Decimal128& decimal, int32_t scale) { + Real x = static_cast(decimal.high_bits()) * Derived::two_to_64(); + x += static_cast(decimal.low_bits()); + if (scale >= -38 && scale <= 38) { + x *= Derived::powers_of_ten()[-scale + 38]; + } else { + x *= std::pow(static_cast(10), static_cast(-scale)); + } + return x; + } + + static Real ToReal(Decimal128 decimal, int32_t scale) { + if (decimal.high_bits() < 0) { + // Convert the absolute value to avoid precision loss + decimal.Negate(); + return -ToRealPositive(decimal, scale); + } else { + return ToRealPositive(decimal, scale); + } + } }; -struct Decimal128FromFloat : public Decimal128FromReal { +struct DecimalFloatConversion + : public DecimalRealConversion { static constexpr const float* powers_of_ten() { return kFloatPowersOfTen; } + + static constexpr float two_to_64() { return 1.8446744e+19f; } }; -struct Decimal128FromDouble : public Decimal128FromReal { +struct DecimalDoubleConversion + : public DecimalRealConversion { static constexpr const double* powers_of_ten() { return kDoublePowersOfTen; } + + static constexpr double two_to_64() { return 1.8446744073709552e+19; } }; } // namespace Result Decimal128::FromReal(float x, int32_t precision, int32_t scale) { - return Decimal128FromFloat::FromReal(x, precision, scale); + return DecimalFloatConversion::FromReal(x, precision, scale); } Result Decimal128::FromReal(double x, int32_t precision, int32_t scale) { - return Decimal128FromDouble::FromReal(x, precision, scale); + return DecimalDoubleConversion::FromReal(x, precision, scale); +} + +float Decimal128::ToFloat(int32_t scale) const { + return DecimalFloatConversion::ToReal(*this, scale); +} + +double Decimal128::ToDouble(int32_t scale) const { + return DecimalDoubleConversion::ToReal(*this, scale); } std::string Decimal128::ToIntegerString() const { diff --git a/cpp/src/arrow/util/decimal.h b/cpp/src/arrow/util/decimal.h index d20a24620e1..1f727057c13 100644 --- a/cpp/src/arrow/util/decimal.h +++ b/cpp/src/arrow/util/decimal.h @@ -138,12 +138,38 @@ class ARROW_EXPORT Decimal128 : public BasicDecimal128 { return ToInteger().Value(out); } + /// \brief Convert to a floating-point number (scaled) + float ToFloat(int32_t scale) const; + /// \brief Convert to a floating-point number (scaled) + double ToDouble(int32_t scale) const; + + /// \brief Convert to a floating-point number (scaled) + template + T ToReal(int32_t scale) const { + return ToRealConversion::ToReal(*this, scale); + } + friend ARROW_EXPORT std::ostream& operator<<(std::ostream& os, const Decimal128& decimal); private: /// Converts internal error code to Status Status ToArrowStatus(DecimalStatus dstatus) const; + + template + struct ToRealConversion {}; +}; + +template <> +struct Decimal128::ToRealConversion { + static float ToReal(const Decimal128& dec, int32_t scale) { return dec.ToFloat(scale); } +}; + +template <> +struct Decimal128::ToRealConversion { + static double ToReal(const Decimal128& dec, int32_t scale) { + return dec.ToDouble(scale); + } }; } // namespace arrow diff --git a/cpp/src/arrow/util/decimal_test.cc b/cpp/src/arrow/util/decimal_test.cc index e80eaab4f3f..3419526375c 100644 --- a/cpp/src/arrow/util/decimal_test.cc +++ b/cpp/src/arrow/util/decimal_test.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -462,7 +463,7 @@ INSTANTIATE_TEST_SUITE_P( // 2**64 - 2**40 (exactly representable in a float) FromFloatTestParam{1.8446743e+19f, 20, 0, "18446742974197923840"}, FromFloatTestParam{-1.8446743e+19f, 20, 0, "-18446742974197923840"}, - // 2**64 + 2**41 (exactly representable in a double) + // 2**64 + 2**41 (exactly representable in a float) FromFloatTestParam{1.8446746e+19f, 20, 0, "18446746272732807168"}, FromFloatTestParam{-1.8446746e+19f, 20, 0, "-18446746272732807168"}, FromFloatTestParam{1.8446746e+15f, 20, 4, "1844674627273280.7168"}, @@ -540,6 +541,153 @@ TEST(TestDecimalFromRealDouble, LargeValues) { } } +template +struct ToRealTestParam { + std::string decimal_value; + int32_t scale; + Real expected; +}; + +using ToFloatTestParam = ToRealTestParam; +using ToDoubleTestParam = ToRealTestParam; + +template +void CheckDecimalToReal(const std::string& decimal_value, int32_t scale, Real expected) { + Decimal128 dec(decimal_value); + ASSERT_EQ(dec.ToReal(scale), expected); +} + +void CheckFloatToRealApprox(const std::string& decimal_value, int32_t scale, + float expected) { + Decimal128 dec(decimal_value); + ASSERT_FLOAT_EQ(dec.ToReal(scale), expected); +} + +void CheckDoubleToRealApprox(const std::string& decimal_value, int32_t scale, + double expected) { + Decimal128 dec(decimal_value); + ASSERT_DOUBLE_EQ(dec.ToReal(scale), expected); +} + +// Common tests for Decimal128::ToReal +template +class TestDecimalToReal : public ::testing::Test { + public: + using Real = T; + using ParamType = ToRealTestParam; + + Real Pow2(int exp) { return std::pow(static_cast(2), static_cast(exp)); } + + Real Pow10(int exp) { return std::pow(static_cast(10), static_cast(exp)); } + + void TestSuccess() { + const std::vector params{ + // clang-format off + {"0", 0, 0.0f}, + {"0", 10, 0.0f}, + {"0", -10, 0.0f}, + {"1", 0, 1.0f}, + {"12345", 0, 12345.f}, +#ifndef __MINGW32__ // MinGW has precision issues + {"12345", 1, 1234.5f}, +#endif + {"12345", -3, 12345000.f}, + // 2**62 + {"4611686018427387904", 0, Pow2(62)}, + // 2**63 + 2**62 + {"13835058055282163712", 0, Pow2(63) + Pow2(62)}, + // 2**64 + 2**62 + {"23058430092136939520", 0, Pow2(64) + Pow2(62)}, + // 10**38 - 2**103 +#ifndef __MINGW32__ // MinGW has precision issues + {"99999989858795198174164788026374356992", 0, Pow10(38) - Pow2(103)}, +#endif + // clang-format on + }; + for (const ParamType& param : params) { + CheckDecimalToReal(param.decimal_value, param.scale, param.expected); + if (param.decimal_value != "0") { + CheckDecimalToReal("-" + param.decimal_value, param.scale, -param.expected); + } + } + } +}; + +TYPED_TEST_SUITE(TestDecimalToReal, RealTypes); + +TYPED_TEST(TestDecimalToReal, TestSuccess) { this->TestSuccess(); } + +// Custom test for Decimal128::ToReal +class TestDecimalToRealFloat : public TestDecimalToReal {}; + +TEST_F(TestDecimalToRealFloat, LargeValues) { + // Note that exact comparisons would succeed on some platforms (Linux, macOS). + // Nevertheless, power-of-ten factors are not all exactly representable + // in binary floating point. + for (int32_t scale = -38; scale <= 38; scale++) { + CheckFloatToRealApprox("1", scale, Pow10(-scale)); + } + for (int32_t scale = -38; scale <= 36; scale++) { + const Real factor = static_cast(123); + CheckFloatToRealApprox("123", scale, factor * Pow10(-scale)); + } +} + +TEST_F(TestDecimalToRealFloat, Precision) { + // 2**63 + 2**40 (exactly representable in a float's 24 bits of precision) + CheckDecimalToReal("9223373136366403584", 0, 9.223373e+18f); + CheckDecimalToReal("-9223373136366403584", 0, -9.223373e+18f); + // 2**64 + 2**41 (exactly representable in a float) + CheckDecimalToReal("18446746272732807168", 0, 1.8446746e+19f); + CheckDecimalToReal("-18446746272732807168", 0, -1.8446746e+19f); +} + +// ToReal tests are disabled on MinGW because of precision issues in results +#ifndef __MINGW32__ + +// Custom test for Decimal128::ToReal +class TestDecimalToRealDouble : public TestDecimalToReal {}; + +TEST_F(TestDecimalToRealDouble, LargeValues) { + // Note that exact comparisons would succeed on some platforms (Linux, macOS). + // Nevertheless, power-of-ten factors are not all exactly representable + // in binary floating point. + for (int32_t scale = -308; scale <= 308; scale++) { + CheckDoubleToRealApprox("1", scale, Pow10(-scale)); + } + for (int32_t scale = -308; scale <= 306; scale++) { + const Real factor = static_cast(123); + CheckDoubleToRealApprox("123", scale, factor * Pow10(-scale)); + } +} + +TEST_F(TestDecimalToRealDouble, Precision) { + // 2**63 + 2**11 (exactly representable in a double's 53 bits of precision) + CheckDecimalToReal("9223372036854777856", 0, 9.223372036854778e+18); + CheckDecimalToReal("-9223372036854777856", 0, -9.223372036854778e+18); + // 2**64 - 2**11 (exactly representable in a double) + CheckDecimalToReal("18446744073709549568", 0, 1.844674407370955e+19); + CheckDecimalToReal("-18446744073709549568", 0, -1.844674407370955e+19); + // 2**64 + 2**11 (exactly representable in a double) + CheckDecimalToReal("18446744073709555712", 0, 1.8446744073709556e+19); + CheckDecimalToReal("-18446744073709555712", 0, -1.8446744073709556e+19); + // Almost 10**38 (minus 2**73) + CheckDecimalToReal("99999999999999978859343891977453174784", 0, + 9.999999999999998e+37); + CheckDecimalToReal("-99999999999999978859343891977453174784", 0, + -9.999999999999998e+37); + CheckDecimalToReal("99999999999999978859343891977453174784", 10, + 9.999999999999998e+27); + CheckDecimalToReal("-99999999999999978859343891977453174784", 10, + -9.999999999999998e+27); + CheckDecimalToReal("99999999999999978859343891977453174784", -10, + 9.999999999999998e+47); + CheckDecimalToReal("-99999999999999978859343891977453174784", -10, + -9.999999999999998e+47); +} + +#endif // __MINGW32__ + TEST(Decimal128Test, TestSmallNumberFormat) { Decimal128 value("0.2"); std::string expected("0.2");