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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cpp/src/arrow/compute/api_scalar.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -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")
Comment thread
fangchenli marked this conversation as resolved.
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")
Expand Down
36 changes: 36 additions & 0 deletions cpp/src/arrow/compute/api_scalar.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand Down
167 changes: 167 additions & 0 deletions cpp/src/arrow/compute/kernels/base_arithmetic_internal.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,7 @@ namespace arrow {

using internal::AddWithOverflow;
using internal::DivideWithOverflow;
using internal::ModuloWithOverflow;
using internal::MultiplyWithOverflow;
using internal::NegateWithOverflow;
using internal::SubtractWithOverflow;
Expand DownExpand Up@@ -468,6 +469,172 @@ struct FloatingDivideChecked {
// TODO: Add decimal
};

// Remainder (truncated): result has same sign as dividend (C/C++ semantics)
struct Remainder {
template <typename T, typename Arg0, typename Arg1>
static enable_if_floating_value<T> Call(KernelContext*, Arg0 left, Arg1 right,
Status*) {
return std::fmod(left, right);
}

template <typename T, typename Arg0, typename Arg1>
static enable_if_integer_value<T> 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 <typename T, typename Arg0, typename Arg1>
static enable_if_decimal_value<T> 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 <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<T, Arg0>::value && std::is_same<T, Arg1>::value, "");
if (ARROW_PREDICT_FALSE(right == 0)) {
*st = Status::Invalid("divide by zero");
return 0;
}
return std::fmod(left, 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<T, Arg0>::value && std::is_same<T, Arg1>::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 <typename T, typename Arg0, typename Arg1>
static enable_if_decimal_value<T> Call(KernelContext* ctx, Arg0 left, Arg1 right,
Status* st) {
return Remainder::Call<T>(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 <typename T>
T AdjustRemainderToFloored(T rem, T right) {
if constexpr (std::is_signed_v<T>) {
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 <typename T, typename Arg0, typename Arg1>
static enable_if_floating_value<T> 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 <typename T, typename Arg0, typename Arg1>
static enable_if_integer_value<T> 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 <typename T, typename Arg0, typename Arg1>
static enable_if_decimal_value<T> 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 <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<T, Arg0>::value && std::is_same<T, Arg1>::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 <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<T, Arg0>::value && std::is_same<T, Arg1>::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 <typename T, typename Arg0, typename Arg1>
static enable_if_decimal_value<T> Call(KernelContext* ctx, Arg0 left, Arg1 right,
Status* st) {
return Modulo::Call<T>(ctx, left, right, st);
}
};

struct Negate {
template <typename T, typename Arg>
static constexpr enable_if_floating_value<T> Call(KernelContext*, Arg arg, Status*) {
Expand Down
76 changes: 75 additions & 1 deletion cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -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(
Expand DownExpand Up@@ -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") {
Expand DownExpand Up@@ -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);
Expand DownExpand Up@@ -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"
Expand DownExpand Up@@ -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());
Expand Down
Loading
Loading