This is an automated email from the ASF dual-hosted git repository.
rok pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 5183e21386f GH-46901: [C++][Compute] Add remainder and modulo kernels
(#48914)
5183e21386f is described below
commit 5183e21386f841dc17790b7b4808a5c8924729a6
Author: Fangchen Li <[email protected]>
AuthorDate: Thu Oct 1 07:41:16 2026 -0700
GH-46901: [C++][Compute] Add remainder and modulo kernels (#48914)
### Rationale for this change
Arrow is currently missing remainder and modulo kernels.
### What changes are included in this PR?
Add the kernels `remainder`, `remainder_checked`, `modulo`, and
`modulo_checked`, following the terminology in the [divmod
proposal](https://lists.apache.org/thread/h3t6nz1ys2k2hnbrjvwyoxkf70cps8sh):
* `remainder` uses truncated (C/C++) semantics — the result has the sign of
the dividend, e.g. `remainder(-7, 3) == -1`.
* `modulo` uses floored (Python/R) semantics — the result has the sign of
the divisor, e.g. `modulo(-7, 3) == 2`.
Both are supported for integer, floating-point and decimal inputs, and are
exposed as `compute::Remainder` / `compute::Modulo` in `api_scalar.h`.
Decimal arguments are promoted to a common scale like `add`, but the result
type is resolved by a dedicated resolver rather than reusing
`ResolveDecimalAdditionOrSubtractionOutput`: 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.
Left as follow-ups: combined divmod #27909, floor division #39386, and
duration support.
### Are these changes tested?
Yes. New tests cover truncated vs. floored semantics across signed/unsigned
integer, floating-point and decimal types, division by zero, `INT_MIN % -1`
overflow, and decimal result-type resolution (same type in/out, mixed
precisions and scales, Decimal128/Decimal256 promotion, decimal/integer
promotion, and maximum-precision decimals).
### Are there any user-facing changes?
Yes — four new compute functions (`remainder`, `remainder_checked`,
`modulo`, `modulo_checked`), the corresponding `compute::Remainder` /
`compute::Modulo` C++ APIs, and a new `BasicDecimal256 operator%`. These are
additions only; no existing API is changed or removed.
* GitHub Issue: #46901
* xref: https://github.com/apache/arrow/pull/46902,
* xref: https://github.com/apache/arrow/pull/28497
Lead-authored-by: Fangchen Li <[email protected]>
Co-authored-by: Rok Mihevc <[email protected]>
Co-authored-by: tadeja <[email protected]>
Signed-off-by: Rok Mihevc <[email protected]>
---
cpp/src/arrow/compute/api_scalar.cc | 2 +
cpp/src/arrow/compute/api_scalar.h | 36 +++
.../compute/kernels/base_arithmetic_internal.h | 75 +++++
cpp/src/arrow/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 | 117 ++++---
docs/source/python/api/compute.rst | 4 +
python/pyarrow/tests/test_compute.py | 12 +
11 files changed, 648 insertions(+), 48 deletions(-)
diff --git a/cpp/src/arrow/compute/api_scalar.cc
b/cpp/src/arrow/compute/api_scalar.cc
index 0aa8fd757a9..f3b3dfbfbdf 100644
--- a/cpp/src/arrow/compute/api_scalar.cc
+++ b/cpp/src/arrow/compute/api_scalar.cc
@@ -799,8 +799,10 @@ Result<Datum> 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 c4238b956c9..5acc8c7bb6b 100644
--- a/cpp/src/arrow/compute/api_scalar.h
+++ b/cpp/src/arrow/compute/api_scalar.h
@@ -671,6 +671,42 @@ Result<Datum> 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<Datum> 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<Datum> 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 320c40373c5..5b22fde6025 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,80 @@ struct FloatingDivideChecked {
// TODO: Add decimal
};
+enum class RemainderMode { kTruncated, kFloored };
+
+// Truncated: sign follows the dividend (C/C++ '%').
+// Floored: sign follows the divisor (Python '%').
+template <RemainderMode Mode, typename T>
+T FinishRemainder(T rem, T divisor) {
+ if constexpr (Mode == RemainderMode::kTruncated) {
+ return rem;
+ } else {
+ if constexpr (std::is_floating_point_v<T>) {
+ if (rem == 0) {
+ // Zero takes the sign of the divisor
+ return std::copysign(rem, divisor);
+ }
+ }
+ if constexpr (!std::is_unsigned_v<T>) {
+ const T zero{};
+ if ((rem > zero && divisor < zero) || (rem < zero && divisor > zero)) {
+ rem += divisor;
+ }
+ }
+ return rem;
+ }
+}
+
+template <RemainderMode Mode, bool Checked>
+struct RemainderImpl {
+ template <typename T, typename Arg0, typename Arg1>
+ static enable_if_floating_value<T> Call(KernelContext*, Arg0 left, Arg1
right,
+ Status* st) {
+ static_assert(std::is_same_v<T, Arg0> && std::is_same_v<T, Arg1>);
+ if constexpr (Checked) {
+ if (ARROW_PREDICT_FALSE(right == 0)) {
+ *st = Status::Invalid("divide by zero");
+ return T{};
+ }
+ }
+ return FinishRemainder<Mode, T>(std::fmod(left, right), right);
+ }
+
+ template <typename T, typename Arg0, typename Arg1>
+ static enable_if_integer_value<T> Call(KernelContext*, Arg0 left, Arg1 right,
+ Status* st) {
+ static_assert(std::is_same_v<T, Arg0> && std::is_same_v<T, Arg1>);
+ T result{};
+ if (ARROW_PREDICT_FALSE(ModuloWithOverflow(left, right, &result))) {
+ if (right == 0) {
+ *st = Status::Invalid("divide by zero");
+ } else if constexpr (Checked) {
+ *st = Status::Invalid("overflow");
+ }
+ // INT_MIN % -1 is 0 when unchecked
+ return T{};
+ }
+ return FinishRemainder<Mode, T>(result, right);
+ }
+
+ template <typename T, typename Arg0, typename Arg1>
+ static enable_if_decimal_value<T> Call(KernelContext*, Arg0 left, Arg1 right,
+ Status* st) {
+ static_assert(std::is_same_v<T, Arg0> && std::is_same_v<T, Arg1>);
+ if (ARROW_PREDICT_FALSE(right == T{})) {
+ *st = Status::Invalid("divide by zero");
+ return T{};
+ }
+ return FinishRemainder<Mode, T>(left % right, right);
+ }
+};
+
+using Remainder = RemainderImpl<RemainderMode::kTruncated, /*Checked=*/false>;
+using RemainderChecked = RemainderImpl<RemainderMode::kTruncated,
/*Checked=*/true>;
+using Modulo = RemainderImpl<RemainderMode::kFloored, /*Checked=*/false>;
+using ModuloChecked = RemainderImpl<RemainderMode::kFloored, /*Checked=*/true>;
+
struct Negate {
template <typename T, typename Arg>
static constexpr enable_if_floating_value<T> 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 cd60d5280b1..14dbd5b7e8b 100644
--- a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
@@ -613,6 +613,19 @@ Result<TypeHolder>
ResolveDecimalAdditionOrSubtractionOutput(
});
}
+Result<TypeHolder> ResolveDecimalRemainderOutput(KernelContext*,
+ const
std::vector<TypeHolder>& types) {
+ return ResolveDecimalBinaryOperationOutput(
+ types,
+ [](int32_t p1, int32_t s1, int32_t p2,
+ int32_t s2) -> Result<std::pair<int32_t, int32_t>> {
+ 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<TypeHolder> ResolveDecimalMultiplicationOutput(
KernelContext*, const std::vector<TypeHolder>& 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",
remainder_doc);
+ AddDecimalBinaryKernels<Remainder>("remainder", remainder.get());
+ DCHECK_OK(registry->AddFunction(std::move(remainder)));
+
+ // ----------------------------------------------------------------------
+ auto remainder_checked = MakeArithmeticFunctionNotNull<RemainderChecked>(
+ "remainder_checked", remainder_checked_doc);
+ AddDecimalBinaryKernels<RemainderChecked>("remainder_checked",
remainder_checked.get());
+ DCHECK_OK(registry->AddFunction(std::move(remainder_checked)));
+
+ // ----------------------------------------------------------------------
+ auto modulo = MakeArithmeticFunctionNotNull<Modulo>("modulo", modulo_doc);
+ AddDecimalBinaryKernels<Modulo>("modulo", modulo.get());
+ DCHECK_OK(registry->AddFunction(std::move(modulo)));
+
+ // ----------------------------------------------------------------------
+ auto modulo_checked =
+ MakeArithmeticFunctionNotNull<ModuloChecked>("modulo_checked",
modulo_checked_doc);
+ AddDecimalBinaryKernels<ModuloChecked>("modulo_checked",
modulo_checked.get());
+ DCHECK_OK(registry->AddFunction(std::move(modulo_checked)));
+
// ----------------------------------------------------------------------
auto negate = MakeUnaryArithmeticFunction<Negate>("negate", negate_doc);
AddDecimalUnaryKernels<Negate>(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 11ba956071b..68bb1cb22cb 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<CType>::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]");
+}
+
+// ============== MODULO (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]");
+ // Negative dividend and positive divisor
+ 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<CType>::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 eddb1aae7b2..9e3de37f3f9 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<BasicDecimal128, 128>;
template class GenericBasicDecimal<BasicDecimal256, 256>;
diff --git a/cpp/src/arrow/util/basic_decimal.h
b/cpp/src/arrow/util/basic_decimal.h
index 638c4870f1d..f35696f4ee5 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 69714a935a4..c9d49aaab1f 100644
--- a/cpp/src/arrow/util/int_util_overflow.h
+++ b/cpp/src/arrow/util/int_util_overflow.h
@@ -137,6 +137,25 @@ template <typename Int>
return false;
}
+template <typename Int>
+[[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<Int>) {
+ constexpr auto kMin = std::numeric_limits<Int>::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 f24fea20a9d..15d426dca6c 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,20 @@ 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. Division by zero
+ returns an error for integer and decimal inputs. For floating-point inputs
+ it returns ``NaN`` in ``modulo`` and an error in ``modulo_checked``. 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. Division by
+ zero returns an error for integer and decimal inputs. For floating-point
+ inputs it returns ``NaN`` in ``remainder`` and an error in
+ ``remainder_checked``. Decimal arguments are promoted to a common scale
+ ``s``; the result then has ``scale = s`` and ``precision = max(p1, p2)``.
+
Bit-wise functions
~~~~~~~~~~~~~~~~~~
diff --git a/docs/source/python/api/compute.rst
b/docs/source/python/api/compute.rst
index 6a4b04468d4..2c4da3abd18 100644
--- a/docs/source/python/api/compute.rst
+++ b/docs/source/python/api/compute.rst
@@ -96,12 +96,16 @@ throws an ``ArrowInvalid`` exception when overflow is
detected.
exp
expm1
hypot
+ modulo
+ modulo_checked
multiply
multiply_checked
negate
negate_checked
power
power_checked
+ remainder
+ remainder_checked
sign
sqrt
sqrt_checked
diff --git a/python/pyarrow/tests/test_compute.py
b/python/pyarrow/tests/test_compute.py
index 797fbc220ec..a146278a8b1 100644
--- a/python/pyarrow/tests/test_compute.py
+++ b/python/pyarrow/tests/test_compute.py
@@ -1949,6 +1949,18 @@ def test_arithmetic_multiply():
assert result.equals(expected)
+def test_arithmetic_remainder_modulo():
+ left = pa.array([7, -7, 7, -7, None])
+ right = pa.array([3, 3, -3, -3, 3])
+ expected_remainder = [1, -1, 1, -1, None]
+ expected_modulo = [1, 2, -2, -1, None]
+
+ assert pc.remainder(left, right).to_pylist() == expected_remainder
+ assert pc.remainder_checked(left, right).to_pylist() == expected_remainder
+ assert pc.modulo(left, right).to_pylist() == expected_modulo
+ assert pc.modulo_checked(left, right).to_pylist() == expected_modulo
+
+
@pytest.mark.parametrize("ty", ["round", "round_to_multiple"])
def test_round_to_integer(ty):
if ty == "round":