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":

Reply via email to