Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,34 @@ struct CastFunctor<Decimal128Type, I, enable_if_t<is_floating_type<I>::value>> {
}
};

// ----------------------------------------------------------------------
// Decimal to real

struct DecimalToReal {
template <typename RealType, typename ARG0>
RealType Call(KernelContext* ctx, const Decimal128& val) const {
return val.ToReal<RealType>(in_scale_);
}

int32_t in_scale_;
};

template <typename O>
struct CastFunctor<O, Decimal128Type, enable_if_t<is_floating_type<O>::value>> {
static void Exec(KernelContext* ctx, const ExecBatch& batch, Datum* out) {
const auto& in_type_inst =
checked_cast<const Decimal128Type&>(*batch[0].array()->type);
const auto in_scale = in_type_inst.scale();

applicator::ScalarUnaryNotNullStateful<O, Decimal128Type, DecimalToReal> kernel(
DecimalToReal{in_scale});
return kernel.Exec(ctx, batch, out);
}
};

// ----------------------------------------------------------------------
// Top-level kernel instantiation

namespace {

template <typename OutType>
Expand Down Expand Up @@ -558,8 +586,12 @@ std::shared_ptr<CastFunction> 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<OutType>(out_ty, func.get());

// From decimal to floating point
DCHECK_OK(func->AddKernel(Type::DECIMAL, {InputType::Array(Type::DECIMAL)}, out_ty,
CastFunctor<OutType, Decimal128Type>::Exec));
return func;
}

Expand Down
22 changes: 22 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_cast_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<DataType>& 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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
45 changes: 40 additions & 5 deletions cpp/src/arrow/util/decimal.cc
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ static constexpr double kDoublePowersOfTen[2 * 38 + 1] = {
namespace {

template <typename Real, typename Derived>
struct Decimal128FromReal {
struct DecimalRealConversion {
static Result<Decimal128> FromPositiveReal(Real real, int32_t precision,
int32_t scale) {
auto x = real;
Expand Down Expand Up @@ -140,24 +140,59 @@ struct Decimal128FromReal {
return FromPositiveReal(x, precision, scale);
}
}

static Real ToRealPositive(const Decimal128& decimal, int32_t scale) {
Real x = static_cast<Real>(decimal.high_bits()) * Derived::two_to_64();
x += static_cast<Real>(decimal.low_bits());
if (scale >= -38 && scale <= 38) {
x *= Derived::powers_of_ten()[-scale + 38];
} else {
x *= std::pow(static_cast<Real>(10), static_cast<Real>(-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<float, Decimal128FromFloat> {
struct DecimalFloatConversion
: public DecimalRealConversion<float, DecimalFloatConversion> {
static constexpr const float* powers_of_ten() { return kFloatPowersOfTen; }

static constexpr float two_to_64() { return 1.8446744e+19f; }
};

struct Decimal128FromDouble : public Decimal128FromReal<double, Decimal128FromDouble> {
struct DecimalDoubleConversion
: public DecimalRealConversion<double, DecimalDoubleConversion> {
static constexpr const double* powers_of_ten() { return kDoublePowersOfTen; }

static constexpr double two_to_64() { return 1.8446744073709552e+19; }
};

} // namespace

Result<Decimal128> Decimal128::FromReal(float x, int32_t precision, int32_t scale) {
return Decimal128FromFloat::FromReal(x, precision, scale);
return DecimalFloatConversion::FromReal(x, precision, scale);
}

Result<Decimal128> 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 {
Expand Down
26 changes: 26 additions & 0 deletions cpp/src/arrow/util/decimal.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,38 @@ class ARROW_EXPORT Decimal128 : public BasicDecimal128 {
return ToInteger<T>().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 <typename T>
T ToReal(int32_t scale) const {
return ToRealConversion<T>::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 <typename T>
struct ToRealConversion {};
};

template <>
struct Decimal128::ToRealConversion<float> {
static float ToReal(const Decimal128& dec, int32_t scale) { return dec.ToFloat(scale); }
};

template <>
struct Decimal128::ToRealConversion<double> {
static double ToReal(const Decimal128& dec, int32_t scale) {
return dec.ToDouble(scale);
}
};

} // namespace arrow
150 changes: 149 additions & 1 deletion cpp/src/arrow/util/decimal_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <array>
#include <cmath>
#include <cstdint>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
Expand Down Expand Up @@ -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"},
Expand Down Expand Up @@ -540,6 +541,153 @@ TEST(TestDecimalFromRealDouble, LargeValues) {
}
}

template <typename Real>
struct ToRealTestParam {
std::string decimal_value;
int32_t scale;
Real expected;
};

using ToFloatTestParam = ToRealTestParam<float>;
using ToDoubleTestParam = ToRealTestParam<double>;

template <typename Real>
void CheckDecimalToReal(const std::string& decimal_value, int32_t scale, Real expected) {
Decimal128 dec(decimal_value);
ASSERT_EQ(dec.ToReal<Real>(scale), expected);
}

void CheckFloatToRealApprox(const std::string& decimal_value, int32_t scale,
float expected) {
Decimal128 dec(decimal_value);
ASSERT_FLOAT_EQ(dec.ToReal<float>(scale), expected);
}

void CheckDoubleToRealApprox(const std::string& decimal_value, int32_t scale,
double expected) {
Decimal128 dec(decimal_value);
ASSERT_DOUBLE_EQ(dec.ToReal<double>(scale), expected);
}

// Common tests for Decimal128::ToReal<T>
template <typename T>
class TestDecimalToReal : public ::testing::Test {
public:
using Real = T;
using ParamType = ToRealTestParam<T>;

Real Pow2(int exp) { return std::pow(static_cast<Real>(2), static_cast<Real>(exp)); }

Real Pow10(int exp) { return std::pow(static_cast<Real>(10), static_cast<Real>(exp)); }

void TestSuccess() {
const std::vector<ParamType> 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<Real>(param.decimal_value, param.scale, param.expected);
if (param.decimal_value != "0") {
CheckDecimalToReal<Real>("-" + param.decimal_value, param.scale, -param.expected);
}
}
}
};

TYPED_TEST_SUITE(TestDecimalToReal, RealTypes);

TYPED_TEST(TestDecimalToReal, TestSuccess) { this->TestSuccess(); }

// Custom test for Decimal128::ToReal<float>
class TestDecimalToRealFloat : public TestDecimalToReal<float> {};

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<Real>(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<float>("9223373136366403584", 0, 9.223373e+18f);
CheckDecimalToReal<float>("-9223373136366403584", 0, -9.223373e+18f);
// 2**64 + 2**41 (exactly representable in a float)
CheckDecimalToReal<float>("18446746272732807168", 0, 1.8446746e+19f);
CheckDecimalToReal<float>("-18446746272732807168", 0, -1.8446746e+19f);
}

// ToReal<double> tests are disabled on MinGW because of precision issues in results
#ifndef __MINGW32__

// Custom test for Decimal128::ToReal<double>
class TestDecimalToRealDouble : public TestDecimalToReal<double> {};

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<Real>(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<double>("9223372036854777856", 0, 9.223372036854778e+18);
CheckDecimalToReal<double>("-9223372036854777856", 0, -9.223372036854778e+18);
// 2**64 - 2**11 (exactly representable in a double)
CheckDecimalToReal<double>("18446744073709549568", 0, 1.844674407370955e+19);
CheckDecimalToReal<double>("-18446744073709549568", 0, -1.844674407370955e+19);
// 2**64 + 2**11 (exactly representable in a double)
CheckDecimalToReal<double>("18446744073709555712", 0, 1.8446744073709556e+19);
CheckDecimalToReal<double>("-18446744073709555712", 0, -1.8446744073709556e+19);
// Almost 10**38 (minus 2**73)
CheckDecimalToReal<double>("99999999999999978859343891977453174784", 0,
9.999999999999998e+37);
CheckDecimalToReal<double>("-99999999999999978859343891977453174784", 0,
-9.999999999999998e+37);
CheckDecimalToReal<double>("99999999999999978859343891977453174784", 10,
9.999999999999998e+27);
CheckDecimalToReal<double>("-99999999999999978859343891977453174784", 10,
-9.999999999999998e+27);
CheckDecimalToReal<double>("99999999999999978859343891977453174784", -10,
9.999999999999998e+47);
CheckDecimalToReal<double>("-99999999999999978859343891977453174784", -10,
-9.999999999999998e+47);
}

#endif // __MINGW32__

TEST(Decimal128Test, TestSmallNumberFormat) {
Decimal128 value("0.2");
std::string expected("0.2");
Expand Down