From 2d1d850a3072aabe0013f00d01a4fe97ff387c03 Mon Sep 17 00:00:00 2001 From: Fangchen Li Date: Tue, 20 Jan 2026 14:29:08 -0800 Subject: [PATCH] GH-46901: [C++][Compute] Add remainder and modulo kernels Add the `remainder`/`remainder_checked` (truncated, sign follows the dividend) and `modulo`/`modulo_checked` (floored, sign follows the divisor) scalar arithmetic kernels for integer, floating-point and decimal inputs. Decimal arguments are promoted to a common scale like `add`, but the result type is resolved by a dedicated resolver: a remainder is always smaller in magnitude than the divisor, so no extra digit is needed for a carry and the result is `precision = max(p1, p2)`, `scale = s1`. This keeps maximum-precision inputs (decimal128(38, 0), decimal256(76, 0)) from overflowing the decimal precision range. Co-authored-by: tadeja <864005+tadeja@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NpB1vVJngVjyyWvWn4AUm7 --- cpp/src/arrow/compute/api_scalar.cc | 2 + cpp/src/arrow/compute/api_scalar.h | 36 ++ .../kernels/base_arithmetic_internal.h | 167 +++++++++ .../compute/kernels/scalar_arithmetic.cc | 76 +++- .../compute/kernels/scalar_arithmetic_test.cc | 344 ++++++++++++++++++ cpp/src/arrow/util/basic_decimal.cc | 8 + cpp/src/arrow/util/basic_decimal.h | 2 + cpp/src/arrow/util/int_util_overflow.h | 20 + docs/source/cpp/compute.rst | 115 +++--- 9 files changed, 722 insertions(+), 48 deletions(-) diff --git a/cpp/src/arrow/compute/api_scalar.cc b/cpp/src/arrow/compute/api_scalar.cc index 0aa8fd757a98..f3b3dfbfbdfa 100644 --- a/cpp/src/arrow/compute/api_scalar.cc +++ b/cpp/src/arrow/compute/api_scalar.cc @@ -799,8 +799,10 @@ Result RoundToMultiple(const Datum& arg, RoundToMultipleOptions options, SCALAR_ARITHMETIC_BINARY(Add, "add", "add_checked") SCALAR_ARITHMETIC_BINARY(Divide, "divide", "divide_checked") SCALAR_ARITHMETIC_BINARY(Logb, "logb", "logb_checked") +SCALAR_ARITHMETIC_BINARY(Modulo, "modulo", "modulo_checked") SCALAR_ARITHMETIC_BINARY(Multiply, "multiply", "multiply_checked") SCALAR_ARITHMETIC_BINARY(Power, "power", "power_checked") +SCALAR_ARITHMETIC_BINARY(Remainder, "remainder", "remainder_checked") SCALAR_ARITHMETIC_BINARY(ShiftLeft, "shift_left", "shift_left_checked") SCALAR_ARITHMETIC_BINARY(ShiftRight, "shift_right", "shift_right_checked") SCALAR_ARITHMETIC_BINARY(Subtract, "subtract", "subtract_checked") diff --git a/cpp/src/arrow/compute/api_scalar.h b/cpp/src/arrow/compute/api_scalar.h index c4238b956c9c..5acc8c7bb6b2 100644 --- a/cpp/src/arrow/compute/api_scalar.h +++ b/cpp/src/arrow/compute/api_scalar.h @@ -671,6 +671,42 @@ Result Divide(const Datum& left, const Datum& right, ArithmeticOptions options = ArithmeticOptions(), ExecContext* ctx = NULLPTR); +/// \brief Compute the remainder (truncated division) of two values. +/// Array values must be the same length. If either argument is null the result +/// will be null. For integer and decimal types, if there is a zero divisor, an +/// error will be raised. For floating-point types, a zero divisor yields NaN +/// unless overflow checking is enabled, in which case an error is raised. +/// +/// The result has the same sign as the dividend (C/C++ semantics). +/// +/// \param[in] left the dividend +/// \param[in] right the divisor +/// \param[in] options arithmetic options (enable/disable overflow checking), optional +/// \param[in] ctx the function execution context, optional +/// \return the elementwise remainder +ARROW_EXPORT +Result Remainder(const Datum& left, const Datum& right, + ArithmeticOptions options = ArithmeticOptions(), + ExecContext* ctx = NULLPTR); + +/// \brief Compute the modulo (floored division) of two values. +/// Array values must be the same length. If either argument is null the result +/// will be null. For integer and decimal types, if there is a zero divisor, an +/// error will be raised. For floating-point types, a zero divisor yields NaN +/// unless overflow checking is enabled, in which case an error is raised. +/// +/// The result has the same sign as the divisor (Python semantics). +/// +/// \param[in] left the dividend +/// \param[in] right the divisor +/// \param[in] options arithmetic options (enable/disable overflow checking), optional +/// \param[in] ctx the function execution context, optional +/// \return the elementwise modulo +ARROW_EXPORT +Result Modulo(const Datum& left, const Datum& right, + ArithmeticOptions options = ArithmeticOptions(), + ExecContext* ctx = NULLPTR); + /// \brief Negate values. /// /// If argument is null the result will be null. diff --git a/cpp/src/arrow/compute/kernels/base_arithmetic_internal.h b/cpp/src/arrow/compute/kernels/base_arithmetic_internal.h index b4840061ae75..ff789ca9d3ff 100644 --- a/cpp/src/arrow/compute/kernels/base_arithmetic_internal.h +++ b/cpp/src/arrow/compute/kernels/base_arithmetic_internal.h @@ -34,6 +34,7 @@ namespace arrow { using internal::AddWithOverflow; using internal::DivideWithOverflow; +using internal::ModuloWithOverflow; using internal::MultiplyWithOverflow; using internal::NegateWithOverflow; using internal::SubtractWithOverflow; @@ -468,6 +469,172 @@ struct FloatingDivideChecked { // TODO: Add decimal }; +// Remainder (truncated): result has same sign as dividend (C/C++ semantics) +struct Remainder { + template + static enable_if_floating_value Call(KernelContext*, Arg0 left, Arg1 right, + Status*) { + return std::fmod(left, right); + } + + template + static enable_if_integer_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + T result; + if (ARROW_PREDICT_FALSE(ModuloWithOverflow(left, right, &result))) { + if (right == 0) { + *st = Status::Invalid("divide by zero"); + } else { + // INT_MIN % -1 overflow case, result is 0 + result = 0; + } + } + return result; + } + + template + static enable_if_decimal_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + if (right == Arg1()) { + *st = Status::Invalid("divide by zero"); + return T(); + } + return left % right; + } +}; + +struct RemainderChecked { + template + static enable_if_floating_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + static_assert(std::is_same::value && std::is_same::value, ""); + if (ARROW_PREDICT_FALSE(right == 0)) { + *st = Status::Invalid("divide by zero"); + return 0; + } + return std::fmod(left, right); + } + + template + static enable_if_integer_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + static_assert(std::is_same::value && std::is_same::value, ""); + T result; + if (ARROW_PREDICT_FALSE(ModuloWithOverflow(left, right, &result))) { + if (right == 0) { + *st = Status::Invalid("divide by zero"); + } else { + *st = Status::Invalid("overflow"); + } + } + return result; + } + + template + static enable_if_decimal_value Call(KernelContext* ctx, Arg0 left, Arg1 right, + Status* st) { + return Remainder::Call(ctx, left, right, st); + } +}; + +// Helper: Convert truncated remainder to floored modulo for signed types. +// Floored modulo has the same sign as the divisor (Python semantics). +template +T AdjustRemainderToFloored(T rem, T right) { + if constexpr (std::is_signed_v) { + if ((rem > 0 && right < 0) || (rem < 0 && right > 0)) { + rem += right; + } + } + return rem; +} + +// Modulo (floored): result has same sign as divisor (Python semantics) +struct Modulo { + template + static enable_if_floating_value Call(KernelContext*, Arg0 left, Arg1 right, + Status*) { + T rem = std::fmod(left, right); + if (rem == 0) { + // Preserve the sign based on divisor for zero results + return std::copysign(rem, right); + } + return AdjustRemainderToFloored(rem, right); + } + + template + static enable_if_integer_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + T result; + if (ARROW_PREDICT_FALSE(ModuloWithOverflow(left, right, &result))) { + if (right == 0) { + *st = Status::Invalid("divide by zero"); + } else { + // INT_MIN % -1 overflow case, result is 0 + result = 0; + } + return result; + } + return AdjustRemainderToFloored(result, right); + } + + template + static enable_if_decimal_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + static const T kZero{}; + if (right == kZero) { + *st = Status::Invalid("divide by zero"); + return T(); + } + T rem = left % right; + // Convert truncated to floored: adjust if signs differ + if ((rem > kZero && right < kZero) || (rem < kZero && right > kZero)) { + rem = rem + right; + } + return rem; + } +}; + +struct ModuloChecked { + template + static enable_if_floating_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + static_assert(std::is_same::value && std::is_same::value, ""); + if (ARROW_PREDICT_FALSE(right == 0)) { + *st = Status::Invalid("divide by zero"); + return 0; + } + T rem = std::fmod(left, right); + if (rem == 0) { + // Preserve the sign based on divisor for zero results + return std::copysign(rem, right); + } + return AdjustRemainderToFloored(rem, right); + } + + template + static enable_if_integer_value Call(KernelContext*, Arg0 left, Arg1 right, + Status* st) { + static_assert(std::is_same::value && std::is_same::value, ""); + T result; + if (ARROW_PREDICT_FALSE(ModuloWithOverflow(left, right, &result))) { + if (right == 0) { + *st = Status::Invalid("divide by zero"); + } else { + *st = Status::Invalid("overflow"); + } + return result; + } + return AdjustRemainderToFloored(result, right); + } + + template + static enable_if_decimal_value Call(KernelContext* ctx, Arg0 left, Arg1 right, + Status* st) { + return Modulo::Call(ctx, left, right, st); + } +}; + struct Negate { template static constexpr enable_if_floating_value Call(KernelContext*, Arg arg, Status*) { diff --git a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc index cd60d5280b1c..14dbd5b7e8bd 100644 --- a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc +++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc @@ -613,6 +613,19 @@ Result ResolveDecimalAdditionOrSubtractionOutput( }); } +Result ResolveDecimalRemainderOutput(KernelContext*, + const std::vector& types) { + return ResolveDecimalBinaryOperationOutput( + types, + [](int32_t p1, int32_t s1, int32_t p2, + int32_t s2) -> Result> { + DCHECK_EQ(s1, s2); + // Unlike addition, no extra digit is needed: the magnitude of a + // remainder is always less than that of the divisor. + return std::make_pair(std::max(p1, p2), s1); + }); +} + Result ResolveDecimalMultiplicationOutput( KernelContext*, const std::vector& types) { return ResolveDecimalBinaryOperationOutput( @@ -674,6 +687,9 @@ void AddDecimalBinaryKernels(const std::string& name, ScalarFunction* func) { if (op == "add" || op == "subtract") { out_type = OutputType(ResolveDecimalAdditionOrSubtractionOutput); constraint = DecimalsHaveSameScale(); + } else if (op == "remainder" || op == "modulo") { + out_type = OutputType(ResolveDecimalRemainderOutput); + constraint = DecimalsHaveSameScale(); } else if (op == "multiply") { out_type = OutputType(ResolveDecimalMultiplicationOutput); } else if (op == "divide") { @@ -784,7 +800,7 @@ struct ArithmeticFunction : ScalarFunction { // "add_checked" -> "add" const auto func_name = name(); const std::string op = func_name.substr(0, func_name.find("_")); - if (op == "add" || op == "subtract") { + if (op == "add" || op == "subtract" || op == "remainder" || op == "modulo") { return CastBinaryDecimalArgs(DecimalPromotion::kAdd, types); } else if (op == "multiply") { return CastBinaryDecimalArgs(DecimalPromotion::kMultiply, types); @@ -1173,6 +1189,42 @@ const FunctionDoc div_checked_doc{ "integer overflow is encountered."), {"dividend", "divisor"}}; +const FunctionDoc remainder_doc{ + "Compute the remainder of the arguments element-wise", + ("The result has the same sign as the dividend (truncated division).\n" + "This is equivalent to the C/C++ '%' operator.\n" + "Integer and decimal division by zero returns an error, while\n" + "floating-point division by zero returns NaN.\n" + "Use function \"remainder_checked\" if you want to get an error\n" + "in all the aforementioned cases."), + {"dividend", "divisor"}}; + +const FunctionDoc remainder_checked_doc{ + "Compute the remainder of the arguments element-wise", + ("The result has the same sign as the dividend (truncated division).\n" + "This is equivalent to the C/C++ '%' operator.\n" + "An error is returned when trying to divide by zero, or when\n" + "integer overflow is encountered."), + {"dividend", "divisor"}}; + +const FunctionDoc modulo_doc{ + "Compute the modulo of the arguments element-wise", + ("The result has the same sign as the divisor (floored division).\n" + "This is equivalent to Python's '%' operator.\n" + "Integer and decimal division by zero returns an error, while\n" + "floating-point division by zero returns NaN.\n" + "Use function \"modulo_checked\" if you want to get an error\n" + "in all the aforementioned cases."), + {"dividend", "divisor"}}; + +const FunctionDoc modulo_checked_doc{ + "Compute the modulo of the arguments element-wise", + ("The result has the same sign as the divisor (floored division).\n" + "This is equivalent to Python's '%' operator.\n" + "An error is returned when trying to divide by zero, or when\n" + "integer overflow is encountered."), + {"dividend", "divisor"}}; + const FunctionDoc negate_doc{"Negate the argument element-wise", ("Results will wrap around on integer overflow.\n" "Use function \"negate_checked\" if you want overflow\n" @@ -1724,6 +1776,28 @@ void RegisterScalarArithmetic(FunctionRegistry* registry) { DCHECK_OK(registry->AddFunction(std::move(divide_checked))); + // ---------------------------------------------------------------------- + auto remainder = MakeArithmeticFunctionNotNull("remainder", remainder_doc); + AddDecimalBinaryKernels("remainder", remainder.get()); + DCHECK_OK(registry->AddFunction(std::move(remainder))); + + // ---------------------------------------------------------------------- + auto remainder_checked = MakeArithmeticFunctionNotNull( + "remainder_checked", remainder_checked_doc); + AddDecimalBinaryKernels("remainder_checked", remainder_checked.get()); + DCHECK_OK(registry->AddFunction(std::move(remainder_checked))); + + // ---------------------------------------------------------------------- + auto modulo = MakeArithmeticFunctionNotNull("modulo", modulo_doc); + AddDecimalBinaryKernels("modulo", modulo.get()); + DCHECK_OK(registry->AddFunction(std::move(modulo))); + + // ---------------------------------------------------------------------- + auto modulo_checked = + MakeArithmeticFunctionNotNull("modulo_checked", modulo_checked_doc); + AddDecimalBinaryKernels("modulo_checked", modulo_checked.get()); + DCHECK_OK(registry->AddFunction(std::move(modulo_checked))); + // ---------------------------------------------------------------------- auto negate = MakeUnaryArithmeticFunction("negate", negate_doc); AddDecimalUnaryKernels(negate.get()); diff --git a/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc b/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc index 11ba956071b1..3b46e5da785b 100644 --- a/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc @@ -938,6 +938,173 @@ TYPED_TEST(TestBinaryArithmeticSigned, DivideOverflowRaises) { this->AssertBinop(Divide, MakeArray(min), MakeArray(-1), "[0]"); } +// ============== REMAINDER (Truncated) Tests ============== + +TYPED_TEST(TestBinaryArithmeticIntegral, Remainder) { + for (auto check_overflow : {false, true}) { + this->SetOverflowCheck(check_overflow); + // Empty arrays + this->AssertBinop(Remainder, "[]", "[]", "[]"); + // Basic positive cases + this->AssertBinop(Remainder, "[7, 10, 20]", "[3, 4, 7]", "[1, 2, 6]"); + // Array with nulls + this->AssertBinop(Remainder, "[null, 10, 30, null, 20]", "[1, 4, 2, 5, 10]", + "[null, 2, 0, null, 0]"); + // Scalar % Array + this->AssertBinop(Remainder, 33, "[null, 1, 3, null, 2]", "[null, 0, 0, null, 1]"); + // Array % Scalar + this->AssertBinop(Remainder, "[null, 10, 30, null, 2]", 3, "[null, 1, 0, null, 2]"); + // Scalar % Scalar + this->AssertBinop(Remainder, 16, 7, 2); + } +} + +TYPED_TEST(TestBinaryArithmeticSigned, Remainder) { + // Truncated semantics: sign follows dividend + this->AssertBinop(Remainder, "[7]", "[3]", "[1]"); + this->AssertBinop(Remainder, "[-7]", "[3]", "[-1]"); + this->AssertBinop(Remainder, "[7]", "[-3]", "[1]"); + this->AssertBinop(Remainder, "[-7]", "[-3]", "[-1]"); + // Mixed array + this->AssertBinop(Remainder, "[-3, 2, -7, 10]", "[1, 1, 2, 3]", "[0, 0, -1, 1]"); +} + +TYPED_TEST(TestBinaryArithmeticUnsigned, Remainder) { + this->AssertBinop(Remainder, "[7, 100, 255]", "[3, 30, 16]", "[1, 10, 15]"); +} + +TYPED_TEST(TestBinaryArithmeticSigned, RemainderOverflow) { + using CType = typename TestFixture::CType; + auto min = std::numeric_limits::lowest(); + + // Unchecked: returns 0 (the mathematically correct result) + this->SetOverflowCheck(false); + this->AssertBinop(Remainder, MakeArray(min), MakeArray(CType(-1)), "[0]"); + + // Checked: raises overflow error + this->SetOverflowCheck(true); + this->AssertBinopRaises(Remainder, MakeArray(min), MakeArray(CType(-1)), "overflow"); +} + +TYPED_TEST(TestBinaryArithmeticIntegral, RemainderByZero) { + for (auto check_overflow : {false, true}) { + this->SetOverflowCheck(check_overflow); + this->AssertBinopRaises(Remainder, "[3, 2, 6]", "[1, 1, 0]", "divide by zero"); + } +} + +TYPED_TEST(TestBinaryArithmeticFloating, Remainder) { + SKIP_IF_HALF_FLOAT(); + + this->SetNansEqual(true); + + // Basic cases + this->AssertBinop(Remainder, "[7.5, 10.0]", "[2.5, 3.0]", "[0.0, 1.0]"); + // Negative numbers - truncated semantics: sign follows dividend + this->AssertBinop(Remainder, "[-7.5]", "[2.5]", "[-0.0]"); + this->AssertBinop(Remainder, "[7.5]", "[-2.5]", "[0.0]"); + this->AssertBinop(Remainder, "[-7.5]", "[-2.5]", "[-0.0]"); + + // Division by zero returns NaN (unchecked) + this->SetOverflowCheck(false); + this->AssertBinop(Remainder, "[1.0]", "[0.0]", "[NaN]"); + + // Division by zero raises error (checked) + this->SetOverflowCheck(true); + this->AssertBinopRaises(Remainder, "[1.0]", "[0.0]", "divide by zero"); + + // Infinity edge cases (unchecked) + this->SetOverflowCheck(false); + this->AssertBinop(Remainder, "[Inf]", "[2.0]", "[NaN]"); + this->AssertBinop(Remainder, "[-Inf]", "[2.0]", "[NaN]"); + this->AssertBinop(Remainder, "[2.0]", "[Inf]", "[2.0]"); + this->AssertBinop(Remainder, "[2.0]", "[-Inf]", "[2.0]"); + this->AssertBinop(Remainder, "[Inf]", "[Inf]", "[NaN]"); +} + +// ============== MOD (Floored) Tests ============== + +TYPED_TEST(TestBinaryArithmeticIntegral, Modulo) { + for (auto check_overflow : {false, true}) { + this->SetOverflowCheck(check_overflow); + // Empty arrays + this->AssertBinop(Modulo, "[]", "[]", "[]"); + // Basic positive cases (same as remainder for positive numbers) + this->AssertBinop(Modulo, "[7, 10, 20]", "[3, 4, 7]", "[1, 2, 6]"); + // Array with nulls + this->AssertBinop(Modulo, "[null, 10, 30, null, 20]", "[1, 4, 2, 5, 10]", + "[null, 2, 0, null, 0]"); + // Scalar % Array + this->AssertBinop(Modulo, 33, "[null, 1, 3, null, 2]", "[null, 0, 0, null, 1]"); + // Array % Scalar + this->AssertBinop(Modulo, "[null, 10, 30, null, 2]", 3, "[null, 1, 0, null, 2]"); + } +} + +TYPED_TEST(TestBinaryArithmeticSigned, Modulo) { + // Floored semantics: sign follows divisor + this->AssertBinop(Modulo, "[7]", "[3]", "[1]"); + this->AssertBinop(Modulo, "[-7]", "[3]", "[2]"); + this->AssertBinop(Modulo, "[7]", "[-3]", "[-2]"); + this->AssertBinop(Modulo, "[-7]", "[-3]", "[-1]"); + // Edge case: -1 mod positive + this->AssertBinop(Modulo, "[-1]", "[3]", "[2]"); +} + +TYPED_TEST(TestBinaryArithmeticUnsigned, Modulo) { + // Same as remainder for unsigned (no negative numbers) + this->AssertBinop(Modulo, "[7, 100, 255]", "[3, 30, 16]", "[1, 10, 15]"); +} + +TYPED_TEST(TestBinaryArithmeticSigned, ModuloOverflow) { + using CType = typename TestFixture::CType; + auto min = std::numeric_limits::lowest(); + + // Unchecked: returns 0 + this->SetOverflowCheck(false); + this->AssertBinop(Modulo, MakeArray(min), MakeArray(CType(-1)), "[0]"); + + // Checked: raises overflow error + this->SetOverflowCheck(true); + this->AssertBinopRaises(Modulo, MakeArray(min), MakeArray(CType(-1)), "overflow"); +} + +TYPED_TEST(TestBinaryArithmeticIntegral, ModuloByZero) { + for (auto check_overflow : {false, true}) { + this->SetOverflowCheck(check_overflow); + this->AssertBinopRaises(Modulo, "[3, 2, 6]", "[1, 1, 0]", "divide by zero"); + } +} + +TYPED_TEST(TestBinaryArithmeticFloating, Modulo) { + SKIP_IF_HALF_FLOAT(); + + this->SetNansEqual(true); + + // Basic cases + this->AssertBinop(Modulo, "[7.5, 10.0]", "[2.5, 3.0]", "[0.0, 1.0]"); + // Negative numbers - floored semantics: sign follows divisor + this->AssertBinop(Modulo, "[-7.5]", "[2.5]", "[0.0]"); + this->AssertBinop(Modulo, "[7.5]", "[-2.5]", "[-0.0]"); + this->AssertBinop(Modulo, "[-7.5]", "[-2.5]", "[-0.0]"); + + // Division by zero returns NaN (unchecked) + this->SetOverflowCheck(false); + this->AssertBinop(Modulo, "[1.0]", "[0.0]", "[NaN]"); + + // Division by zero raises error (checked) + this->SetOverflowCheck(true); + this->AssertBinopRaises(Modulo, "[1.0]", "[0.0]", "divide by zero"); + + // Infinity edge cases (unchecked) + this->SetOverflowCheck(false); + this->AssertBinop(Modulo, "[Inf]", "[2.0]", "[NaN]"); + this->AssertBinop(Modulo, "[-Inf]", "[2.0]", "[NaN]"); + this->AssertBinop(Modulo, "[2.0]", "[Inf]", "[2.0]"); + this->AssertBinop(Modulo, "[2.0]", "[-Inf]", "[-Inf]"); // floored: 2.0 + (-Inf) = -Inf + this->AssertBinop(Modulo, "[Inf]", "[Inf]", "[NaN]"); +} + TYPED_TEST(TestBinaryArithmeticFloating, Power) { SKIP_IF_HALF_FLOAT(); @@ -2409,6 +2576,183 @@ TEST_F(TestBinaryArithmeticDecimal, Divide) { } } +TEST_F(TestBinaryArithmeticDecimal, Remainder) { + // Truncated semantics: sign follows dividend + + // array array, decimal128 + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["7.00", "-7.00", "7.00", "-7.00"])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["3.00", "3.00", "-3.00", "-3.00"])"); + auto expected = + ArrayFromJSON(decimal128(5, 2), R"(["1.00", "-1.00", "1.00", "-1.00"])"); + CheckScalarBinary("remainder", left, right, expected); + } + + // array array, decimal256 + { + auto left = ArrayFromJSON(decimal256(5, 2), R"(["7.00", "-7.00"])"); + auto right = ArrayFromJSON(decimal256(5, 2), R"(["3.00", "3.00"])"); + auto expected = ArrayFromJSON(decimal256(5, 2), R"(["1.00", "-1.00"])"); + CheckScalarBinary("remainder", left, right, expected); + } + + // scalar scalar + { + auto left = ScalarFromJSON(decimal128(5, 2), R"("17.50")"); + auto right = ScalarFromJSON(decimal128(5, 2), R"("5.00")"); + auto expected = ScalarFromJSON(decimal128(5, 2), R"("2.50")"); + CheckScalarBinary("remainder", left, right, expected); + } + + // null handling + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["7.00", null, null])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["3.00", "3.00", null])"); + auto expected = ArrayFromJSON(decimal128(5, 2), R"(["1.00", null, null])"); + CheckScalarBinary("remainder", left, right, expected); + } + + // failed case: divide by 0 + { + auto left = ScalarFromJSON(decimal256(1, 0), R"("7")"); + auto right = ScalarFromJSON(decimal256(1, 0), R"("0")"); + ASSERT_RAISES(Invalid, CallFunction("remainder", {left, right})); + } +} + +TEST_F(TestBinaryArithmeticDecimal, Modulo) { + // Floored semantics: sign follows divisor + + // array array, decimal128 + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["7.00", "-7.00", "7.00", "-7.00"])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["3.00", "3.00", "-3.00", "-3.00"])"); + auto expected = + ArrayFromJSON(decimal128(5, 2), R"(["1.00", "2.00", "-2.00", "-1.00"])"); + CheckScalarBinary("modulo", left, right, expected); + } + + // array array, decimal256 + { + auto left = ArrayFromJSON(decimal256(5, 2), R"(["7.00", "-7.00"])"); + auto right = ArrayFromJSON(decimal256(5, 2), R"(["3.00", "3.00"])"); + auto expected = ArrayFromJSON(decimal256(5, 2), R"(["1.00", "2.00"])"); + CheckScalarBinary("modulo", left, right, expected); + } + + // scalar scalar + { + auto left = ScalarFromJSON(decimal128(5, 2), R"("-17.50")"); + auto right = ScalarFromJSON(decimal128(5, 2), R"("5.00")"); + auto expected = ScalarFromJSON(decimal128(5, 2), R"("2.50")"); + CheckScalarBinary("modulo", left, right, expected); + } + + // null handling + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["-7.00", null, null])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["3.00", "3.00", null])"); + auto expected = ArrayFromJSON(decimal128(5, 2), R"(["2.00", null, null])"); + CheckScalarBinary("modulo", left, right, expected); + } + + // failed case: divide by 0 + { + auto left = ScalarFromJSON(decimal256(1, 0), R"("7")"); + auto right = ScalarFromJSON(decimal256(1, 0), R"("0")"); + ASSERT_RAISES(Invalid, CallFunction("modulo", {left, right})); + } +} + +// The output of remainder/modulo never exceeds the magnitude of the divisor, so +// unlike add/subtract the result precision needs no extra digit for a carry. +TEST_F(TestBinaryArithmeticDecimal, RemainderAndModuloResultType) { + for (const auto& func : {"remainder", "modulo"}) { + ARROW_SCOPED_TRACE(func); + + // same type in, same type out + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["17.00"])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["5.00"])"); + auto expected = ArrayFromJSON(decimal128(5, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + + // mixed precision, same scale: precision = max(p1, p2) + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["17.00"])"); + auto right = ArrayFromJSON(decimal128(3, 2), R"(["5.00"])"); + auto expected = ArrayFromJSON(decimal128(5, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + { + auto left = ArrayFromJSON(decimal128(3, 2), R"(["7.00"])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["5.00"])"); + auto expected = ArrayFromJSON(decimal128(5, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + + // mixed scale: both arguments are scaled up to max(s1, s2) first + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["17.00"])"); + auto right = ArrayFromJSON(decimal128(4, 1), R"(["5.0"])"); + auto expected = ArrayFromJSON(decimal128(5, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + { + auto left = ArrayFromJSON(decimal128(4, 1), R"(["17.0"])"); + auto right = ArrayFromJSON(decimal128(6, 3), R"(["5.000"])"); + auto expected = ArrayFromJSON(decimal128(6, 3), R"(["2.000"])"); + CheckScalarBinary(func, left, right, expected); + } + + // decimal128 and decimal256 promote to decimal256 + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["17.00"])"); + auto right = ArrayFromJSON(decimal256(5, 2), R"(["5.00"])"); + auto expected = ArrayFromJSON(decimal256(5, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + { + auto left = ArrayFromJSON(decimal256(40, 2), R"(["17.00"])"); + auto right = ArrayFromJSON(decimal128(5, 2), R"(["5.00"])"); + auto expected = ArrayFromJSON(decimal256(40, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + + // decimal and integer promote to decimal + { + auto left = ArrayFromJSON(decimal128(5, 2), R"(["17.00"])"); + auto right = ArrayFromJSON(int32(), "[5]"); + auto expected = ArrayFromJSON(decimal128(12, 2), R"(["2.00"])"); + CheckScalarBinary(func, left, right, expected); + } + + // maximum precision inputs must not overflow the result precision + { + auto left = ScalarFromJSON(decimal128(38, 0), R"("7")"); + auto right = ScalarFromJSON(decimal128(38, 0), R"("3")"); + auto expected = ScalarFromJSON(decimal128(38, 0), R"("1")"); + CheckScalarBinary(func, left, right, expected); + } + { + auto left = ScalarFromJSON(decimal256(76, 0), R"("7")"); + auto right = ScalarFromJSON(decimal256(76, 0), R"("3")"); + auto expected = ScalarFromJSON(decimal256(76, 0), R"("1")"); + CheckScalarBinary(func, left, right, expected); + } + } + + // maximum precision, negative dividend: the two kernels differ + { + auto left = ScalarFromJSON(decimal256(76, 0), R"("-7")"); + auto right = ScalarFromJSON(decimal256(76, 0), R"("3")"); + CheckScalarBinary("remainder", left, right, + ScalarFromJSON(decimal256(76, 0), R"("-1")")); + CheckScalarBinary("modulo", left, right, ScalarFromJSON(decimal256(76, 0), R"("2")")); + } +} + TEST_F(TestBinaryArithmeticDecimal, Atan2) { // Decimal arguments promoted to double, sanity check here const auto func = "atan2"; diff --git a/cpp/src/arrow/util/basic_decimal.cc b/cpp/src/arrow/util/basic_decimal.cc index eddb1aae7b2d..9e3de37f3f9f 100644 --- a/cpp/src/arrow/util/basic_decimal.cc +++ b/cpp/src/arrow/util/basic_decimal.cc @@ -1399,6 +1399,14 @@ BasicDecimal256 operator/(const BasicDecimal256& left, const BasicDecimal256& ri return result; } +BasicDecimal256 operator%(const BasicDecimal256& left, const BasicDecimal256& right) { + BasicDecimal256 remainder; + BasicDecimal256 result; + auto s = left.Divide(right, &result, &remainder); + DCHECK_EQ(s, DecimalStatus::kSuccess); + return remainder; +} + // Explicitly instantiate template base class, for DLL linking on Windows template class GenericBasicDecimal; template class GenericBasicDecimal; diff --git a/cpp/src/arrow/util/basic_decimal.h b/cpp/src/arrow/util/basic_decimal.h index 638c4870f1de..f35696f4ee5f 100644 --- a/cpp/src/arrow/util/basic_decimal.h +++ b/cpp/src/arrow/util/basic_decimal.h @@ -883,5 +883,7 @@ ARROW_EXPORT BasicDecimal256 operator*(const BasicDecimal256& left, const BasicDecimal256& right); ARROW_EXPORT BasicDecimal256 operator/(const BasicDecimal256& left, const BasicDecimal256& right); +ARROW_EXPORT BasicDecimal256 operator%(const BasicDecimal256& left, + const BasicDecimal256& right); } // namespace arrow diff --git a/cpp/src/arrow/util/int_util_overflow.h b/cpp/src/arrow/util/int_util_overflow.h index 69714a935a48..c9d49aaab1fc 100644 --- a/cpp/src/arrow/util/int_util_overflow.h +++ b/cpp/src/arrow/util/int_util_overflow.h @@ -137,6 +137,25 @@ template return false; } +template +[[nodiscard]] bool ModuloWithOverflowGeneric(Int u, Int v, Int* out) { + if (v == 0) { + *out = Int{}; + return true; + } + // INT_MIN % -1 is undefined behavior (the quotient is not representable) and + // traps on some targets, but the mathematical result is 0. + if constexpr (std::is_signed_v) { + constexpr auto kMin = std::numeric_limits::min(); + if (u == kMin && v == -1) { + *out = 0; + return true; + } + } + *out = u % v; + return false; +} + // Define non-generic versions of the above so as to benefit from automatic // integer conversion, to allow for mixed-type calls such as // AddWithOverflow(int32_t, int64_t, int64_t*). @@ -160,6 +179,7 @@ NON_GENERIC_OPS_WITH_OVERFLOW(AddWithOverflow) NON_GENERIC_OPS_WITH_OVERFLOW(SubtractWithOverflow) NON_GENERIC_OPS_WITH_OVERFLOW(MultiplyWithOverflow) NON_GENERIC_OPS_WITH_OVERFLOW(DivideWithOverflow) +NON_GENERIC_OPS_WITH_OVERFLOW(ModuloWithOverflow) #undef NON_GENERIC_OPS_WITH_OVERFLOW #undef NON_GENERIC_OP_WITH_OVERFLOW diff --git a/docs/source/cpp/compute.rst b/docs/source/cpp/compute.rst index 1e067c52188d..3033f855a9bd 100644 --- a/docs/source/cpp/compute.rst +++ b/docs/source/cpp/compute.rst @@ -487,55 +487,64 @@ overflow-checking variant, suffixed ``_checked``, which returns an ``Invalid`` :class:`Status` when overflow is detected. For functions which support decimal inputs (currently ``add``, ``subtract``, -``multiply``, and ``divide`` and their checked variants), decimals of different -precisions/scales will be promoted appropriately. Mixed decimal and -floating-point arguments will cast all arguments to floating-point, while mixed -decimal and integer arguments will cast all arguments to decimals. +``multiply``, ``divide``, ``modulo``, and ``remainder`` and their checked +variants), decimals of different precisions/scales will be promoted +appropriately. Mixed decimal and floating-point arguments will cast all +arguments to floating-point, while mixed decimal and integer arguments will +cast all arguments to decimals. Mixed time resolution temporal inputs will be cast to finest input resolution. -+------------------+--------+-------------------------+-------------------------------+-------+ -| Function name | Arity | Input types | Output type | Notes | -+==================+========+=========================+===============================+=======+ -| abs | Unary | Numeric/Duration | Numeric/Duration | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| abs_checked | Unary | Numeric/Duration | Numeric/Duration | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| add | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| add_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| divide | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| divide_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| exp | Unary | Numeric | Float32/Float64 | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| expm1 | Unary | Numeric | Float32/Float64 | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| hypot | Binary | Numeric | Float32/Float64 | \(3) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| multiply | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| multiply_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| negate | Unary | Numeric/Duration | Numeric/Duration | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| negate_checked | Unary | Signed Numeric/Duration | Signed Numeric/Duration | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| power | Binary | Numeric | Numeric | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| power_checked | Binary | Numeric | Numeric | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| sign | Unary | Numeric/Duration | Int8/Float16/Float32/Float64 | \(2) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| sqrt | Unary | Numeric | Numeric | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| sqrt_checked | Unary | Numeric | Numeric | | -+------------------+--------+-------------------------+-------------------------------+-------+ -| subtract | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ -| subtract_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | -+------------------+--------+-------------------------+-------------------------------+-------+ ++-------------------+--------+-------------------------+-------------------------------+-------+ +| Function name | Arity | Input types | Output type | Notes | ++===================+========+=========================+===============================+=======+ +| abs | Unary | Numeric/Duration | Numeric/Duration | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| abs_checked | Unary | Numeric/Duration | Numeric/Duration | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| add | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| add_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| divide | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| divide_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| exp | Unary | Numeric | Float32/Float64 | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| expm1 | Unary | Numeric | Float32/Float64 | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| hypot | Binary | Numeric | Float32/Float64 | \(3) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| modulo | Binary | Numeric | Numeric | \(4) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| modulo_checked | Binary | Numeric | Numeric | \(4) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| multiply | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| multiply_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| negate | Unary | Numeric/Duration | Numeric/Duration | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| negate_checked | Unary | Signed Numeric/Duration | Signed Numeric/Duration | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| power | Binary | Numeric | Numeric | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| power_checked | Binary | Numeric | Numeric | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| remainder | Binary | Numeric | Numeric | \(5) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| remainder_checked | Binary | Numeric | Numeric | \(5) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| sign | Unary | Numeric/Duration | Int8/Float16/Float32/Float64 | \(2) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| sqrt | Unary | Numeric | Numeric | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| sqrt_checked | Unary | Numeric | Numeric | | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| subtract | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ +| subtract_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | ++-------------------+--------+-------------------------+-------------------------------+-------+ * \(1) Precision and scale of computed DECIMAL results @@ -566,6 +575,18 @@ Mixed time resolution temporal inputs will be cast to finest input resolution. intermediate stages of the computation. If either argument is infinite, the result is ``+Inf`` even if the other argument is NaN. +* \(4) Computes the floored modulo, where the result has the same sign as the + divisor. This is equivalent to Python's ``%`` operator. Integer and decimal + division by zero returns an error, while floating-point division by zero + returns NaN. Decimal arguments are promoted to a common scale ``s``; the + result then has ``scale = s`` and ``precision = max(p1, p2)``. + +* \(5) Computes the truncated remainder, where the result has the same sign as + the dividend. This is equivalent to C/C++'s ``%`` operator. Integer and + decimal division by zero returns an error, while floating-point division by + zero returns NaN. Decimal arguments are promoted to a common scale ``s``; + the result then has ``scale = s`` and ``precision = max(p1, p2)``. + Bit-wise functions ~~~~~~~~~~~~~~~~~~