diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc index 1fe26b316362d..b000efd1e028b 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc @@ -479,6 +479,43 @@ struct DecimalConversions { static Decimal256 ConvertOutput(Decimal256&& val) { return val; } }; +template +struct DecimalConversions { + static Decimal32 ConvertInput(InDecimal&& val) { return Decimal32(val.low_bits()); } + static Decimal32 ConvertOutput(Decimal32&& val) { return val; } +}; + +template <> +struct DecimalConversions { + // Convert then scale + static Decimal64 ConvertInput(Decimal32&& val) { return Decimal64(val); } + static Decimal64 ConvertOutput(Decimal64&& val) { return val; } +}; + +template <> +struct DecimalConversions { + static Decimal64 ConvertInput(Decimal64&& val) { return val; } + static Decimal64 ConvertOutput(Decimal64&& val) { return val; } +}; + +template <> +struct DecimalConversions { + // Scale then truncate + static Decimal128 ConvertInput(Decimal128&& val) { return val; } + static Decimal64 ConvertOutput(Decimal128&& val) { + return Decimal64(static_cast(val.low_bits())); + } +}; + +template <> +struct DecimalConversions { + // Scale then truncate + static Decimal256 ConvertInput(Decimal256&& val) { return val; } + static Decimal64 ConvertOutput(Decimal256&& val) { + return Decimal64(static_cast(val.low_bits())); + } +}; + template <> struct DecimalConversions { // Scale then truncate @@ -495,6 +532,20 @@ struct DecimalConversions { static Decimal128 ConvertOutput(Decimal128&& val) { return val; } }; +template <> +struct DecimalConversions { + // convert then scale + static Decimal128 ConvertInput(Decimal64&& val) { return Decimal128(val.value()); } + static Decimal128 ConvertOutput(Decimal128&& val) { return val; } +}; + +template <> +struct DecimalConversions { + // convert then scale + static Decimal128 ConvertInput(Decimal32&& val) { return Decimal128(val.value()); } + static Decimal128 ConvertOutput(Decimal128&& val) { return val; } +}; + struct UnsafeUpscaleDecimal { template OutValue Call(KernelContext*, Arg0Value val, Status*) const { @@ -659,6 +710,18 @@ struct DecimalCastFunctor { } }; +template +struct CastFunctor< + Decimal32Type, I, + enable_if_t::value || is_binary_view_like_type::value>> + : public DecimalCastFunctor {}; + +template +struct CastFunctor< + Decimal64Type, I, + enable_if_t::value || is_binary_view_like_type::value>> + : public DecimalCastFunctor {}; + template struct CastFunctor< Decimal128Type, I, @@ -744,6 +807,10 @@ std::shared_ptr GetCastToInteger(std::string name) { // From decimal to integer DCHECK_OK(func->AddKernel(Type::DECIMAL, {InputType(Type::DECIMAL)}, out_ty, CastFunctor::Exec)); + DCHECK_OK(func->AddKernel(Type::DECIMAL32, {InputType(Type::DECIMAL32)}, out_ty, + CastFunctor::Exec)); + DCHECK_OK(func->AddKernel(Type::DECIMAL64, {InputType(Type::DECIMAL64)}, out_ty, + CastFunctor::Exec)); DCHECK_OK(func->AddKernel(Type::DECIMAL256, {InputType(Type::DECIMAL256)}, out_ty, CastFunctor::Exec)); return func; @@ -772,6 +839,10 @@ std::shared_ptr GetCastToFloating(std::string name) { AddCommonNumberCasts(out_ty, func.get()); // From decimal to floating point + DCHECK_OK(func->AddKernel(Type::DECIMAL32, {InputType(Type::DECIMAL32)}, out_ty, + CastFunctor::Exec)); + DCHECK_OK(func->AddKernel(Type::DECIMAL64, {InputType(Type::DECIMAL64)}, out_ty, + CastFunctor::Exec)); DCHECK_OK(func->AddKernel(Type::DECIMAL, {InputType(Type::DECIMAL)}, out_ty, CastFunctor::Exec)); DCHECK_OK(func->AddKernel(Type::DECIMAL256, {InputType(Type::DECIMAL256)}, out_ty, @@ -780,6 +851,94 @@ std::shared_ptr GetCastToFloating(std::string name) { return func; } +std::shared_ptr GetCastToDecimal32() { + OutputType sig_out_ty(ResolveOutputFromOptions); + + auto func = std::make_shared("cast_decimal32", Type::DECIMAL32); + AddCommonCasts(Type::DECIMAL32, sig_out_ty, func.get()); + + // Cast from floating point + DCHECK_OK(func->AddKernel(Type::FLOAT, {float32()}, sig_out_ty, + CastFunctor::Exec)); + DCHECK_OK(func->AddKernel(Type::DOUBLE, {float64()}, sig_out_ty, + CastFunctor::Exec)); + + // Cast from integer + for (const std::shared_ptr& in_ty : IntTypes()) { + auto exec = GenerateInteger(in_ty->id()); + DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, sig_out_ty, std::move(exec))); + } + + // Cast from other strings + for (const std::shared_ptr& in_ty : BaseBinaryTypes()) { + auto exec = GenerateVarBinaryBase(in_ty->id()); + DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, sig_out_ty, std::move(exec))); + } + for (const std::shared_ptr& in_ty : BinaryViewTypes()) { + auto exec = GenerateVarBinaryViewBase(in_ty->id()); + DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, sig_out_ty, std::move(exec))); + } + + // Cast from other decimal + auto exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL32, {InputType(Type::DECIMAL32)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL64, {InputType(Type::DECIMAL64)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL128, {InputType(Type::DECIMAL128)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL256, {InputType(Type::DECIMAL256)}, sig_out_ty, exec)); + return func; +} + +std::shared_ptr GetCastToDecimal64() { + OutputType sig_out_ty(ResolveOutputFromOptions); + + auto func = std::make_shared("cast_decimal64", Type::DECIMAL64); + AddCommonCasts(Type::DECIMAL64, sig_out_ty, func.get()); + + // Cast from floating point + DCHECK_OK(func->AddKernel(Type::FLOAT, {float32()}, sig_out_ty, + CastFunctor::Exec)); + DCHECK_OK(func->AddKernel(Type::DOUBLE, {float64()}, sig_out_ty, + CastFunctor::Exec)); + + // Cast from integer + for (const std::shared_ptr& in_ty : IntTypes()) { + auto exec = GenerateInteger(in_ty->id()); + DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, sig_out_ty, std::move(exec))); + } + + // Cast from other strings + for (const std::shared_ptr& in_ty : BaseBinaryTypes()) { + auto exec = GenerateVarBinaryBase(in_ty->id()); + DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, sig_out_ty, std::move(exec))); + } + for (const std::shared_ptr& in_ty : BinaryViewTypes()) { + auto exec = GenerateVarBinaryViewBase(in_ty->id()); + DCHECK_OK(func->AddKernel(in_ty->id(), {in_ty}, sig_out_ty, std::move(exec))); + } + + // Cast from other decimal + auto exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL32, {InputType(Type::DECIMAL32)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL64, {InputType(Type::DECIMAL64)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL128, {InputType(Type::DECIMAL128)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL256, {InputType(Type::DECIMAL256)}, sig_out_ty, exec)); + return func; +} + std::shared_ptr GetCastToDecimal128() { OutputType sig_out_ty(ResolveOutputFromOptions); @@ -809,8 +968,14 @@ std::shared_ptr GetCastToDecimal128() { } // Cast from other decimal - auto exec = CastFunctor::Exec; + auto exec = CastFunctor::Exec; // We resolve the output type of this kernel from the CastOptions + DCHECK_OK( + func->AddKernel(Type::DECIMAL32, {InputType(Type::DECIMAL32)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL64, {InputType(Type::DECIMAL64)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; DCHECK_OK( func->AddKernel(Type::DECIMAL128, {InputType(Type::DECIMAL128)}, sig_out_ty, exec)); exec = CastFunctor::Exec; @@ -848,7 +1013,13 @@ std::shared_ptr GetCastToDecimal256() { } // Cast from other decimal - auto exec = CastFunctor::Exec; + auto exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL32, {InputType(Type::DECIMAL32)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; + DCHECK_OK( + func->AddKernel(Type::DECIMAL64, {InputType(Type::DECIMAL64)}, sig_out_ty, exec)); + exec = CastFunctor::Exec; DCHECK_OK( func->AddKernel(Type::DECIMAL128, {InputType(Type::DECIMAL128)}, sig_out_ty, exec)); exec = CastFunctor::Exec; @@ -950,6 +1121,8 @@ std::vector> GetNumericCasts() { auto cast_double = GetCastToFloating("cast_double"); functions.push_back(cast_double); + functions.push_back(GetCastToDecimal32()); + functions.push_back(GetCastToDecimal64()); functions.push_back(GetCastToDecimal128()); functions.push_back(GetCastToDecimal256()); diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_string.cc b/cpp/src/arrow/compute/kernels/scalar_cast_string.cc index 4edf00225d317..7186612d25a76 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_string.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_string.cc @@ -683,7 +683,8 @@ void AddNumberToStringCasts(CastFunction* func) { template void AddDecimalToStringCasts(CastFunction* func) { auto out_ty = TypeTraits::type_singleton(); - for (const auto& in_tid : std::vector{Type::DECIMAL128, Type::DECIMAL256}) { + for (const auto& in_tid : std::vector{Type::DECIMAL32, Type::DECIMAL64, + Type::DECIMAL128, Type::DECIMAL256}) { DCHECK_OK( func->AddKernel(in_tid, {in_tid}, out_ty, GenerateDecimal(in_tid), diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 33a01425508e0..80d5b3c46cae1 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -447,6 +447,159 @@ TEST(Cast, IntToFloating) { CastOptions::Safe(float64())); } +TEST(Cast, Decimal32ToInt) { + auto options = CastOptions::Safe(int32()); + + for (bool allow_int_overflow : {false, true}) { + for (bool allow_decimal_truncate : {false, true}) { + options.allow_int_overflow = allow_int_overflow; + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_overflow_no_truncation = ArrayFromJSON(decimal32(9, 5), R"([ + "02.00000", + "-11.00000", + "22.00000", + "-121.00000", + null])"); + CheckCast(no_overflow_no_truncation, + ArrayFromJSON(int32(), "[2, -11, 22, -121, null]"), options); + } + } + + for (bool allow_int_overflow : {false, true}) { + options.allow_int_overflow = allow_int_overflow; + auto truncation_but_no_overflow = ArrayFromJSON(decimal32(9, 5), R"([ + "02.10000", + "-11.00450", + "22.00045", + "-121.12100", + null])"); + + options.allow_decimal_truncate = true; + CheckCast(truncation_but_no_overflow, + ArrayFromJSON(int32(), "[2, -11, 22, -121, null]"), options); + + options.allow_decimal_truncate = false; + CheckCastFails(truncation_but_no_overflow, options); + } + + for (bool allow_int_overflow : {false, true}) { + for (bool allow_decimal_truncate : {false, true}) { + options.allow_int_overflow = allow_int_overflow; + options.allow_decimal_truncate = allow_decimal_truncate; + + auto overflow_and_truncation = ArrayFromJSON(decimal32(9, 5), R"([ + "1234.00453", + "9999.00344", + null])"); + + if (options.allow_decimal_truncate) { + CheckCast(overflow_and_truncation, ArrayFromJSON(int32(), "[1234, 9999, null]"), + options); + } else { + CheckCastFails(overflow_and_truncation, options); + } + } + } + + Decimal32Builder builder(decimal32(9, -3)); + for (auto d : {Decimal32("12345000."), Decimal32("-12000000.")}) { + ASSERT_OK_AND_ASSIGN(d, d.Rescale(0, -3)); + ASSERT_OK(builder.Append(d)); + } + ASSERT_OK_AND_ASSIGN(auto negative_scale, builder.Finish()); + options.allow_int_overflow = true; + options.allow_decimal_truncate = true; + CheckCast(negative_scale, ArrayFromJSON(int32(), "[12345000, -12000000]"), options); +} + +TEST(Cast, Decimal64ToInt) { + auto options = CastOptions::Safe(int64()); + + for (bool allow_int_overflow : {false, true}) { + for (bool allow_decimal_truncate : {false, true}) { + options.allow_int_overflow = allow_int_overflow; + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_overflow_no_truncation = ArrayFromJSON(decimal64(18, 10), R"([ + "02.0000000000", + "-11.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + CheckCast(no_overflow_no_truncation, + ArrayFromJSON(int64(), "[2, -11, 22, -121, null]"), options); + } + } + + for (bool allow_int_overflow : {false, true}) { + options.allow_int_overflow = allow_int_overflow; + auto truncation_but_no_overflow = ArrayFromJSON(decimal64(18, 10), R"([ + "02.1000000000", + "-11.0000004500", + "22.0000004500", + "-121.1210000000", + null])"); + + options.allow_decimal_truncate = true; + CheckCast(truncation_but_no_overflow, + ArrayFromJSON(int32(), "[2, -11, 22, -121, null]"), options); + + options.allow_decimal_truncate = false; + CheckCastFails(truncation_but_no_overflow, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto overflow_no_truncation = ArrayFromJSON(decimal64(18, 5), R"([ + "1234567890123.00000", + "9999999999999.00000", + null])"); + + options.allow_int_overflow = true; + CheckCast(overflow_no_truncation, + ArrayFromJSON(int64(), "[1234567890123, 9999999999999, null]"), options); + + options.to_type = int32(); + options.allow_int_overflow = false; + CheckCastFails(overflow_no_truncation, options); + } + + for (bool allow_int_overflow : {false, true}) { + for (bool allow_decimal_truncate : {false, true}) { + options.allow_int_overflow = allow_int_overflow; + options.allow_decimal_truncate = allow_decimal_truncate; + options.to_type = int32(); + + auto overflow_and_truncation = ArrayFromJSON(decimal64(18, 5), R"([ + "1234567890123.45345", + "9999999999999.00344", + null])"); + + if (options.allow_int_overflow && options.allow_decimal_truncate) { + CheckCast(overflow_and_truncation, + ArrayFromJSON(int32(), + // 1234567890123 % 2**32, 9999999999999 % 2**32 + "[1912276171, 1316134911, null]"), + options); + } else { + CheckCastFails(overflow_and_truncation, options); + } + } + } + + Decimal64Builder builder(decimal64(18, -4)); + for (auto d : {Decimal64("1234567890000."), Decimal64("-120000.")}) { + ASSERT_OK_AND_ASSIGN(d, d.Rescale(0, -4)); + ASSERT_OK(builder.Append(d)); + } + ASSERT_OK_AND_ASSIGN(auto negative_scale, builder.Finish()); + options.allow_int_overflow = true; + options.allow_decimal_truncate = true; + CheckCast(negative_scale, ArrayFromJSON(int64(), "[1234567890000, -120000]"), options); +} + TEST(Cast, Decimal128ToInt) { auto options = CastOptions::Safe(int64()); @@ -629,11 +782,14 @@ TEST(Cast, Decimal256ToInt) { } TEST(Cast, IntegerToDecimal) { - for (auto decimal_type : {decimal128(22, 2), decimal256(22, 2)}) { + for (auto decimal_type : + {decimal32(9, 2), decimal64(18, 2), decimal128(22, 2), decimal256(22, 2)}) { for (auto integer_type : kIntegerTypes) { - CheckCast( - ArrayFromJSON(integer_type, "[0, 7, null, 100, 99]"), - ArrayFromJSON(decimal_type, R"(["0.00", "7.00", null, "100.00", "99.00"])")); + if (decimal_type->bit_width() > integer_type->bit_width()) { + CheckCast( + ArrayFromJSON(integer_type, "[0, 7, null, 100, 99]"), + ArrayFromJSON(decimal_type, R"(["0.00", "7.00", null, "100.00", "99.00"])")); + } } } @@ -652,6 +808,12 @@ TEST(Cast, IntegerToDecimal) { { CastOptions options; + options.to_type = decimal32(9, 3); + CheckCastFails(ArrayFromJSON(int32(), "[0]"), options); + + options.to_type = decimal64(18, 3); + CheckCastFails(ArrayFromJSON(int64(), "[0]"), options); + options.to_type = decimal128(5, 3); CheckCastFails(ArrayFromJSON(int8(), "[0]"), options); @@ -660,6 +822,166 @@ TEST(Cast, IntegerToDecimal) { } } +TEST(Cast, Decimal32ToDecimal32) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal32(9, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", + null])"); + auto expected = ArrayFromJSON(decimal32(9, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + CheckCast(expected, no_truncation, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal32(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal32(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_4_2, d_5_2, options); + } + + auto d_9_5 = ArrayFromJSON(decimal32(9, 5), R"([ + "-02.12345", + "30.12345", + null])"); + + auto d_6_0 = ArrayFromJSON(decimal32(6, 0), R"([ + "-02.", + "30.", + null])"); + + auto d_9_5_roundtripped = ArrayFromJSON(decimal32(9, 5), R"([ + "-02.00000", + "30.00000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d_9_5, d_6_0, options); + CheckCast(d_6_0, d_9_5_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d_6_0->type(); + CheckCastFails(d_9_5, options); + CheckCast(d_6_0, d_9_5_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d_4_2 = ArrayFromJSON(decimal32(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal32(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal32(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal32(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d_4_2, options); + } +} + +TEST(Cast, Decimal64ToDecimal64) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal64(18, 10), R"([ + "02.0000000000", + "30.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + auto expected = ArrayFromJSON(decimal64(9, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + CheckCast(expected, no_truncation, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal64(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal64(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_4_2, d_5_2, options); + } + + auto d_18_10 = ArrayFromJSON(decimal64(18, 10), R"([ + "-02.1234567890", + "30.1234567890", + null])"); + + auto d_12_0 = ArrayFromJSON(decimal64(12, 0), R"([ + "-02.", + "30.", + null])"); + + auto d_18_10_roundtripped = ArrayFromJSON(decimal64(18, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d_18_10, d_12_0, options); + CheckCast(d_12_0, d_18_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d_12_0->type(); + CheckCastFails(d_18_10, options); + CheckCast(d_12_0, d_18_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d_4_2 = ArrayFromJSON(decimal64(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal64(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal64(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal64(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d_4_2, options); + } +} + TEST(Cast, Decimal128ToDecimal128) { CastOptions options; @@ -820,19 +1142,19 @@ TEST(Cast, Decimal256ToDecimal256) { } } -TEST(Cast, Decimal128ToDecimal256) { +TEST(Cast, Decimal32ToDecimal64) { CastOptions options; for (bool allow_decimal_truncate : {false, true}) { options.allow_decimal_truncate = allow_decimal_truncate; - auto no_truncation = ArrayFromJSON(decimal128(38, 10), R"([ - "02.0000000000", - "30.0000000000", - "22.0000000000", - "-121.0000000000", + auto no_truncation = ArrayFromJSON(decimal32(9, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", null])"); - auto expected = ArrayFromJSON(decimal256(48, 0), R"([ + auto expected = ArrayFromJSON(decimal64(16, 0), R"([ "02.", "30.", "22.", @@ -846,47 +1168,731 @@ TEST(Cast, Decimal128ToDecimal256) { options.allow_decimal_truncate = allow_decimal_truncate; // Same scale, different precision - auto d_5_2 = ArrayFromJSON(decimal128(5, 2), R"([ + auto d_5_2 = ArrayFromJSON(decimal32(5, 2), R"([ "12.34", "0.56"])"); - auto d_4_2 = ArrayFromJSON(decimal256(4, 2), R"([ + auto d_4_2 = ArrayFromJSON(decimal64(4, 2), R"([ "12.34", "0.56"])"); - auto d_40_2 = ArrayFromJSON(decimal256(40, 2), R"([ + auto d_16_2 = ArrayFromJSON(decimal64(16, 2), R"([ "12.34", "0.56"])"); CheckCast(d_5_2, d_4_2, options); - CheckCast(d_5_2, d_40_2, options); + CheckCast(d_5_2, d_16_2, options); } - auto d128_38_10 = ArrayFromJSON(decimal128(38, 10), R"([ - "-02.1234567890", - "30.1234567890", + auto d32_7_5 = ArrayFromJSON(decimal32(7, 5), R"([ + "-02.12345", + "30.12345", null])"); - auto d128_28_0 = ArrayFromJSON(decimal128(28, 0), R"([ + auto d32_9_0 = ArrayFromJSON(decimal32(9, 0), R"([ "-02.", "30.", null])"); - auto d256_28_0 = ArrayFromJSON(decimal256(28, 0), R"([ + auto d64_14_0 = ArrayFromJSON(decimal64(14, 0), R"([ "-02.", "30.", null])"); - auto d256_38_10_roundtripped = ArrayFromJSON(decimal256(38, 10), R"([ + auto d64_18_10_roundtripped = ArrayFromJSON(decimal64(18, 10), R"([ "-02.0000000000", "30.0000000000", null])"); // Rescale which leads to truncation options.allow_decimal_truncate = true; - CheckCast(d128_38_10, d256_28_0, options); - CheckCast(d128_28_0, d256_38_10_roundtripped, options); + CheckCast(d32_7_5, d64_14_0, options); + CheckCast(d32_9_0, d64_18_10_roundtripped, options); options.allow_decimal_truncate = false; - options.to_type = d256_28_0->type(); + options.to_type = d64_14_0->type(); + CheckCastFails(d32_7_5, options); + CheckCast(d32_9_0, d64_18_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d32_4_2 = ArrayFromJSON(decimal32(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal64(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal64(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal64(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d32_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d32_4_2, options); + } +} + +TEST(Cast, Decimal32ToDecimal128) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal32(9, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", + null])"); + auto expected = ArrayFromJSON(decimal128(16, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal32(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal128(4, 2), R"([ + "12.34", + "0.56"])"); + auto d_16_2 = ArrayFromJSON(decimal128(16, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_5_2, d_16_2, options); + } + + auto d32_7_5 = ArrayFromJSON(decimal32(7, 5), R"([ + "-02.12345", + "30.12345", + null])"); + + auto d32_9_0 = ArrayFromJSON(decimal32(9, 0), R"([ + "-02.", + "30.", + null])"); + + auto d128_14_0 = ArrayFromJSON(decimal128(14, 0), R"([ + "-02.", + "30.", + null])"); + + auto d128_38_10_roundtripped = ArrayFromJSON(decimal128(38, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d32_7_5, d128_14_0, options); + CheckCast(d32_9_0, d128_38_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d128_14_0->type(); + CheckCastFails(d32_7_5, options); + CheckCast(d32_9_0, d128_38_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d32_4_2 = ArrayFromJSON(decimal32(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal128(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal128(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal128(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d32_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d32_4_2, options); + } +} + +TEST(Cast, Decimal32ToDecimal256) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal32(9, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", + null])"); + auto expected = ArrayFromJSON(decimal256(16, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal32(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal256(4, 2), R"([ + "12.34", + "0.56"])"); + auto d_16_2 = ArrayFromJSON(decimal256(16, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_5_2, d_16_2, options); + } + + auto d32_7_5 = ArrayFromJSON(decimal32(7, 5), R"([ + "-02.12345", + "30.12345", + null])"); + + auto d32_9_0 = ArrayFromJSON(decimal32(9, 0), R"([ + "-02.", + "30.", + null])"); + + auto d256_14_0 = ArrayFromJSON(decimal256(14, 0), R"([ + "-02.", + "30.", + null])"); + + auto d256_76_10_roundtripped = ArrayFromJSON(decimal256(76, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d32_7_5, d256_14_0, options); + CheckCast(d32_9_0, d256_76_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d256_14_0->type(); + CheckCastFails(d32_7_5, options); + CheckCast(d32_9_0, d256_76_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d32_4_2 = ArrayFromJSON(decimal32(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal256(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal256(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal256(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d32_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d32_4_2, options); + } +} + +TEST(Cast, Decimal64ToDecimal32) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal64(18, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", + null])"); + auto expected = ArrayFromJSON(decimal32(9, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_12_2 = ArrayFromJSON(decimal64(12, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal32(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_12_2, d_4_2, options); + } + + auto d64_15_10 = ArrayFromJSON(decimal64(15, 5), R"([ + "-02.12345", + "30.12345", + null])"); + + auto d64_12_0 = ArrayFromJSON(decimal64(12, 0), R"([ + "-02.", + "30.", + null])"); + + auto d32_6_0 = ArrayFromJSON(decimal32(6, 0), R"([ + "-02.", + "30.", + null])"); + + auto d32_9_5_roundtripped = ArrayFromJSON(decimal32(9, 5), R"([ + "-02.00000", + "30.00000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d64_15_10, d32_6_0, options); + CheckCast(d64_12_0, d32_9_5_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d32_6_0->type(); + CheckCastFails(d64_15_10, options); + CheckCast(d64_12_0, d32_9_5_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d64_4_2 = ArrayFromJSON(decimal64(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal32(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal32(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal32(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d64_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d64_4_2, options); + } +} + +TEST(Cast, Decimal64ToDecimal128) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal64(18, 10), R"([ + "02.0000000000", + "30.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + auto expected = ArrayFromJSON(decimal128(28, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal64(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal128(4, 2), R"([ + "12.34", + "0.56"])"); + auto d_16_2 = ArrayFromJSON(decimal128(16, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_5_2, d_16_2, options); + } + + auto d64_16_10 = ArrayFromJSON(decimal64(16, 10), R"([ + "-02.1234567890", + "30.1234567890", + null])"); + + auto d64_18_0 = ArrayFromJSON(decimal64(18, 0), R"([ + "-02.", + "30.", + null])"); + + auto d128_14_0 = ArrayFromJSON(decimal128(14, 0), R"([ + "-02.", + "30.", + null])"); + + auto d128_38_10_roundtripped = ArrayFromJSON(decimal128(38, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d64_16_10, d128_14_0, options); + CheckCast(d64_18_0, d128_38_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d128_14_0->type(); + CheckCastFails(d64_16_10, options); + CheckCast(d64_18_0, d128_38_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d64_4_2 = ArrayFromJSON(decimal64(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal128(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal128(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal128(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d64_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d64_4_2, options); + } +} + +TEST(Cast, Decimal64ToDecimal256) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal64(18, 10), R"([ + "02.0000000000", + "30.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + auto expected = ArrayFromJSON(decimal256(16, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal64(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal256(4, 2), R"([ + "12.34", + "0.56"])"); + auto d_16_2 = ArrayFromJSON(decimal256(16, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_5_2, d_16_2, options); + } + + auto d64_16_10 = ArrayFromJSON(decimal64(16, 10), R"([ + "-02.1234567890", + "30.1234567890", + null])"); + + auto d64_18_0 = ArrayFromJSON(decimal64(18, 0), R"([ + "-02.", + "30.", + null])"); + + auto d256_14_0 = ArrayFromJSON(decimal256(14, 0), R"([ + "-02.", + "30.", + null])"); + + auto d256_76_10_roundtripped = ArrayFromJSON(decimal256(76, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d64_16_10, d256_14_0, options); + CheckCast(d64_18_0, d256_76_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d256_14_0->type(); + CheckCastFails(d64_16_10, options); + CheckCast(d64_18_0, d256_76_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d64_4_2 = ArrayFromJSON(decimal64(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal256(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal256(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal256(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d64_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d64_4_2, options); + } +} + +TEST(Cast, Decimal128ToDecimal32) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal128(26, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", + null])"); + auto expected = ArrayFromJSON(decimal32(9, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_28_2 = ArrayFromJSON(decimal128(28, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal32(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_28_2, d_4_2, options); + } + + auto d128_28_5 = ArrayFromJSON(decimal128(28, 5), R"([ + "-02.12345", + "30.12345", + null])"); + + auto d128_22_0 = ArrayFromJSON(decimal128(22, 0), R"([ + "-02.", + "30.", + null])"); + + auto d32_7_0 = ArrayFromJSON(decimal32(7, 0), R"([ + "-02.", + "30.", + null])"); + + auto d32_9_5_roundtripped = ArrayFromJSON(decimal32(9, 5), R"([ + "-02.00000", + "30.00000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d128_28_5, d32_7_0, options); + CheckCast(d128_22_0, d32_9_5_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d32_7_0->type(); + CheckCastFails(d128_28_5, options); + CheckCast(d128_22_0, d32_9_5_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d128_4_2 = ArrayFromJSON(decimal128(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal32(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal32(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal32(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d128_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d128_4_2, options); + } +} + +TEST(Cast, Decimal128ToDecimal64) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal128(26, 10), R"([ + "02.0000000000", + "30.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + auto expected = ArrayFromJSON(decimal64(15, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_28_2 = ArrayFromJSON(decimal128(28, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal64(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_28_2, d_4_2, options); + } + + auto d128_28_10 = ArrayFromJSON(decimal128(28, 10), R"([ + "-02.1234567890", + "30.1234567890", + null])"); + + auto d128_22_0 = ArrayFromJSON(decimal128(22, 0), R"([ + "-02.", + "30.", + null])"); + + auto d64_12_0 = ArrayFromJSON(decimal64(12, 0), R"([ + "-02.", + "30.", + null])"); + + auto d64_18_10_roundtripped = ArrayFromJSON(decimal64(18, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d128_28_10, d64_12_0, options); + CheckCast(d128_22_0, d64_18_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d64_12_0->type(); + CheckCastFails(d128_28_10, options); + CheckCast(d128_22_0, d64_18_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d128_4_2 = ArrayFromJSON(decimal128(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal64(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal64(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal64(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d128_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d128_4_2, options); + } +} + +TEST(Cast, Decimal128ToDecimal256) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal128(38, 10), R"([ + "02.0000000000", + "30.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + auto expected = ArrayFromJSON(decimal256(48, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_5_2 = ArrayFromJSON(decimal128(5, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal256(4, 2), R"([ + "12.34", + "0.56"])"); + auto d_40_2 = ArrayFromJSON(decimal256(40, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_5_2, d_4_2, options); + CheckCast(d_5_2, d_40_2, options); + } + + auto d128_38_10 = ArrayFromJSON(decimal128(38, 10), R"([ + "-02.1234567890", + "30.1234567890", + null])"); + + auto d128_28_0 = ArrayFromJSON(decimal128(28, 0), R"([ + "-02.", + "30.", + null])"); + + auto d256_28_0 = ArrayFromJSON(decimal256(28, 0), R"([ + "-02.", + "30.", + null])"); + + auto d256_38_10_roundtripped = ArrayFromJSON(decimal256(38, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d128_38_10, d256_28_0, options); + CheckCast(d128_28_0, d256_38_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d256_28_0->type(); CheckCastFails(d128_38_10, options); CheckCast(d128_28_0, d256_38_10_roundtripped, options); @@ -907,6 +1913,172 @@ TEST(Cast, Decimal128ToDecimal256) { } } +TEST(Cast, Decimal256ToDecimal32) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal256(42, 5), R"([ + "02.00000", + "30.00000", + "22.00000", + "-121.00000", + null])"); + auto expected = ArrayFromJSON(decimal32(9, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_28_2 = ArrayFromJSON(decimal256(42, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal32(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_28_2, d_4_2, options); + } + + auto d256_52_5 = ArrayFromJSON(decimal256(52, 5), R"([ + "-02.12345", + "30.12345", + null])"); + + auto d256_42_0 = ArrayFromJSON(decimal256(42, 0), R"([ + "-02.", + "30.", + null])"); + + auto d32_7_0 = ArrayFromJSON(decimal32(7, 0), R"([ + "-02.", + "30.", + null])"); + + auto d32_9_5_roundtripped = ArrayFromJSON(decimal32(9, 5), R"([ + "-02.00000", + "30.00000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d256_52_5, d32_7_0, options); + CheckCast(d256_42_0, d32_9_5_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d32_7_0->type(); + CheckCastFails(d256_52_5, options); + CheckCast(d256_42_0, d32_9_5_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d256_4_2 = ArrayFromJSON(decimal256(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal32(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal32(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal32(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d256_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d256_4_2, options); + } +} + +TEST(Cast, Decimal256ToDecimal64) { + CastOptions options; + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + auto no_truncation = ArrayFromJSON(decimal256(42, 10), R"([ + "02.0000000000", + "30.0000000000", + "22.0000000000", + "-121.0000000000", + null])"); + auto expected = ArrayFromJSON(decimal64(15, 0), R"([ + "02.", + "30.", + "22.", + "-121.", + null])"); + + CheckCast(no_truncation, expected, options); + } + + for (bool allow_decimal_truncate : {false, true}) { + options.allow_decimal_truncate = allow_decimal_truncate; + + // Same scale, different precision + auto d_42_2 = ArrayFromJSON(decimal256(42, 2), R"([ + "12.34", + "0.56"])"); + auto d_4_2 = ArrayFromJSON(decimal64(4, 2), R"([ + "12.34", + "0.56"])"); + + CheckCast(d_42_2, d_4_2, options); + } + + auto d256_52_10 = ArrayFromJSON(decimal256(52, 10), R"([ + "-02.1234567890", + "30.1234567890", + null])"); + + auto d256_42_0 = ArrayFromJSON(decimal256(42, 0), R"([ + "-02.", + "30.", + null])"); + + auto d64_12_0 = ArrayFromJSON(decimal64(12, 0), R"([ + "-02.", + "30.", + null])"); + + auto d64_18_10_roundtripped = ArrayFromJSON(decimal64(18, 10), R"([ + "-02.0000000000", + "30.0000000000", + null])"); + + // Rescale which leads to truncation + options.allow_decimal_truncate = true; + CheckCast(d256_52_10, d64_12_0, options); + CheckCast(d256_42_0, d64_18_10_roundtripped, options); + + options.allow_decimal_truncate = false; + options.to_type = d64_12_0->type(); + CheckCastFails(d256_52_10, options); + CheckCast(d256_42_0, d64_18_10_roundtripped, options); + + // Precision loss without rescale leads to truncation + auto d256_4_2 = ArrayFromJSON(decimal256(4, 2), R"(["12.34"])"); + for (auto expected : { + ArrayFromJSON(decimal64(3, 2), R"(["12.34"])"), + ArrayFromJSON(decimal64(4, 3), R"(["12.340"])"), + ArrayFromJSON(decimal64(2, 1), R"(["12.3"])"), + }) { + options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto invalid, Cast(d256_4_2, expected->type(), options)); + ASSERT_RAISES(Invalid, invalid.make_array()->ValidateFull()); + + options.allow_decimal_truncate = false; + options.to_type = expected->type(); + CheckCastFails(d256_4_2, options); + } +} + TEST(Cast, Decimal256ToDecimal128) { CastOptions options; @@ -992,7 +2164,8 @@ TEST(Cast, Decimal256ToDecimal128) { TEST(Cast, FloatingToDecimal) { for (auto float_type : {float32(), float64()}) { - for (auto decimal_type : {decimal128(5, 2), decimal256(5, 2)}) { + for (auto decimal_type : + {decimal32(5, 2), decimal64(5, 2), decimal128(5, 2), decimal256(5, 2)}) { CheckCast( ArrayFromJSON(float_type, "[0.0, null, 123.45, 123.456, 999.994]"), ArrayFromJSON(decimal_type, R"(["0.00", null, "123.45", "123.46", "999.99"])")); @@ -1036,7 +2209,8 @@ TEST(Cast, FloatingToDecimal) { TEST(Cast, DecimalToFloating) { for (auto float_type : {float32(), float64()}) { - for (auto decimal_type : {decimal128(5, 2), decimal256(5, 2)}) { + for (auto decimal_type : + {decimal32(5, 2), decimal64(5, 2), decimal128(5, 2), decimal256(5, 2)}) { CheckCast(ArrayFromJSON(decimal_type, R"(["0.00", null, "123.45", "999.99"])"), ArrayFromJSON(float_type, "[0.0, null, 123.45, 999.99]")); } @@ -1048,7 +2222,8 @@ TEST(Cast, DecimalToFloating) { TEST(Cast, DecimalToString) { for (auto string_type : {utf8(), utf8_view(), large_utf8()}) { - for (auto decimal_type : {decimal128(5, 2), decimal256(5, 2)}) { + for (auto decimal_type : + {decimal32(5, 2), decimal64(5, 2), decimal128(5, 2), decimal256(5, 2)}) { CheckCast(ArrayFromJSON(decimal_type, R"(["0.00", null, "123.45", "999.99"])"), ArrayFromJSON(string_type, R"(["0.00", null, "123.45", "999.99"])")); } @@ -1960,7 +3135,8 @@ TEST(Cast, StringToFloating) { TEST(Cast, StringToDecimal) { for (auto string_type : {utf8(), large_utf8()}) { - for (auto decimal_type : {decimal128(5, 2), decimal256(5, 2)}) { + for (auto decimal_type : + {decimal32(5, 2), decimal64(5, 2), decimal128(5, 2), decimal256(5, 2)}) { auto strings = ArrayFromJSON(string_type, R"(["0.01", null, "127.32", "200.43", "0.54"])"); auto decimals = diff --git a/cpp/src/arrow/util/basic_decimal.h b/cpp/src/arrow/util/basic_decimal.h index 9c1f2e479c712..b5404bb7bc6d5 100644 --- a/cpp/src/arrow/util/basic_decimal.h +++ b/cpp/src/arrow/util/basic_decimal.h @@ -739,6 +739,16 @@ class ARROW_EXPORT BasicDecimal256 : public GenericBasicDecimal(value.high_bits()), SignExtend(value.high_bits()), SignExtend(value.high_bits())})) {} + explicit BasicDecimal256(const BasicDecimal64& value) noexcept + : BasicDecimal256(bit_util::little_endian::ToNative( + {value.low_bits(), SignExtend(value.value()), SignExtend(value.value()), + SignExtend(value.value())})) {} + + explicit BasicDecimal256(const BasicDecimal32& value) noexcept + : BasicDecimal256(bit_util::little_endian::ToNative( + {value.low_bits(), SignExtend(value.value()), SignExtend(value.value()), + SignExtend(value.value())})) {} + /// \brief Negate the current value (in-place) BasicDecimal256& Negate();