You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by bk...@apache.org on 2021/06/04 01:43:09 UTC
[arrow] branch master updated: ARROW-12751: [C++] Implement
minimum/maximum kernels
This is an automated email from the ASF dual-hosted git repository.
bkietz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/master by this push:
new e1690d6 ARROW-12751: [C++] Implement minimum/maximum kernels
e1690d6 is described below
commit e1690d6cc9ab1aa56c9e6a4782ee3fe9bd644c06
Author: David Li <li...@gmail.com>
AuthorDate: Thu Jun 3 21:40:29 2021 -0400
ARROW-12751: [C++] Implement minimum/maximum kernels
This is a bit messy, but implements a variadic scalar maximum/minimum kernel.
Closes #10390 from lidavidm/arrow-12751
Authored-by: David Li <li...@gmail.com>
Signed-off-by: Benjamin Kietzman <be...@gmail.com>
---
cpp/src/arrow/compute/api_scalar.cc | 10 +
cpp/src/arrow/compute/api_scalar.h | 29 ++
cpp/src/arrow/compute/kernels/codegen_internal.h | 43 ++-
cpp/src/arrow/compute/kernels/scalar_boolean.cc | 56 ++--
cpp/src/arrow/compute/kernels/scalar_compare.cc | 291 +++++++++++++++++
.../arrow/compute/kernels/scalar_compare_test.cc | 348 +++++++++++++++++++++
docs/source/cpp/compute.rst | 15 +
docs/source/python/api/compute.rst | 8 +
8 files changed, 770 insertions(+), 30 deletions(-)
diff --git a/cpp/src/arrow/compute/api_scalar.cc b/cpp/src/arrow/compute/api_scalar.cc
index 105ba7a..6f77d6f 100644
--- a/cpp/src/arrow/compute/api_scalar.cc
+++ b/cpp/src/arrow/compute/api_scalar.cc
@@ -63,6 +63,16 @@ SCALAR_ARITHMETIC_BINARY(Multiply, "multiply", "multiply_checked")
SCALAR_ARITHMETIC_BINARY(Divide, "divide", "divide_checked")
SCALAR_ARITHMETIC_BINARY(Power, "power", "power_checked")
+Result<Datum> ElementWiseMax(const std::vector<Datum>& args,
+ ElementWiseAggregateOptions options, ExecContext* ctx) {
+ return CallFunction("element_wise_max", args, &options, ctx);
+}
+
+Result<Datum> ElementWiseMin(const std::vector<Datum>& args,
+ ElementWiseAggregateOptions options, ExecContext* ctx) {
+ return CallFunction("element_wise_min", args, &options, ctx);
+}
+
// ----------------------------------------------------------------------
// Set-related operations
diff --git a/cpp/src/arrow/compute/api_scalar.h b/cpp/src/arrow/compute/api_scalar.h
index 0a05b12..ab690f4 100644
--- a/cpp/src/arrow/compute/api_scalar.h
+++ b/cpp/src/arrow/compute/api_scalar.h
@@ -42,6 +42,11 @@ struct ArithmeticOptions : public FunctionOptions {
bool check_overflow;
};
+struct ARROW_EXPORT ElementWiseAggregateOptions : public FunctionOptions {
+ ElementWiseAggregateOptions() : skip_nulls(true) {}
+ bool skip_nulls;
+};
+
struct ARROW_EXPORT MatchSubstringOptions : public FunctionOptions {
explicit MatchSubstringOptions(std::string pattern, bool ignore_case = false)
: pattern(std::move(pattern)), ignore_case(ignore_case) {}
@@ -253,6 +258,30 @@ Result<Datum> Power(const Datum& left, const Datum& right,
ArithmeticOptions options = ArithmeticOptions(),
ExecContext* ctx = NULLPTR);
+/// \brief Find the element-wise maximum of any number of arrays or scalars.
+/// Array values must be the same length.
+///
+/// \param[in] args arrays or scalars to operate on.
+/// \param[in] options options for handling nulls, optional
+/// \param[in] ctx the function execution context, optional
+/// \return the element-wise maximum
+ARROW_EXPORT
+Result<Datum> ElementWiseMax(const std::vector<Datum>& args,
+ ElementWiseAggregateOptions options = {},
+ ExecContext* ctx = NULLPTR);
+
+/// \brief Find the element-wise minimum of any number of arrays or scalars.
+/// Array values must be the same length.
+///
+/// \param[in] args arrays or scalars to operate on.
+/// \param[in] options options for handling nulls, optional
+/// \param[in] ctx the function execution context, optional
+/// \return the element-wise minimum
+ARROW_EXPORT
+Result<Datum> ElementWiseMin(const std::vector<Datum>& args,
+ ElementWiseAggregateOptions options = {},
+ ExecContext* ctx = NULLPTR);
+
/// \brief Compare a numeric array with a scalar.
///
/// \param[in] left datum to compare, must be an Array
diff --git a/cpp/src/arrow/compute/kernels/codegen_internal.h b/cpp/src/arrow/compute/kernels/codegen_internal.h
index e31771a..6d5c837 100644
--- a/cpp/src/arrow/compute/kernels/codegen_internal.h
+++ b/cpp/src/arrow/compute/kernels/codegen_internal.h
@@ -303,8 +303,12 @@ struct BoxScalar;
template <typename Type>
struct BoxScalar<Type, enable_if_has_c_type<Type>> {
using T = typename GetOutputType<Type>::T;
- using ScalarType = typename TypeTraits<Type>::ScalarType;
- static void Box(T val, Scalar* out) { checked_cast<ScalarType*>(out)->value = val; }
+ static void Box(T val, Scalar* out) {
+ // Enables BoxScalar<Int64Type> to work on a (for example) Time64Scalar
+ T* mutable_data = reinterpret_cast<T*>(
+ checked_cast<::arrow::internal::PrimitiveScalarBase*>(out)->mutable_data());
+ *mutable_data = val;
+ }
};
template <typename Type>
@@ -1093,6 +1097,41 @@ ArrayKernelExec GeneratePhysicalInteger(detail::GetTypeId get_id) {
}
}
+template <template <typename... Args> class Generator, typename... Args>
+ArrayKernelExec GeneratePhysicalNumeric(detail::GetTypeId get_id) {
+ switch (get_id.id) {
+ case Type::INT8:
+ return Generator<Int8Type, Args...>::Exec;
+ case Type::INT16:
+ return Generator<Int16Type, Args...>::Exec;
+ case Type::INT32:
+ case Type::DATE32:
+ case Type::TIME32:
+ return Generator<Int32Type, Args...>::Exec;
+ case Type::INT64:
+ case Type::DATE64:
+ case Type::TIMESTAMP:
+ case Type::TIME64:
+ case Type::DURATION:
+ return Generator<Int64Type, Args...>::Exec;
+ case Type::UINT8:
+ return Generator<UInt8Type, Args...>::Exec;
+ case Type::UINT16:
+ return Generator<UInt16Type, Args...>::Exec;
+ case Type::UINT32:
+ return Generator<UInt32Type, Args...>::Exec;
+ case Type::UINT64:
+ return Generator<UInt64Type, Args...>::Exec;
+ case Type::FLOAT:
+ return Generator<FloatType, Args...>::Exec;
+ case Type::DOUBLE:
+ return Generator<DoubleType, Args...>::Exec;
+ default:
+ DCHECK(false);
+ return ExecFail;
+ }
+}
+
// Generate a kernel given a templated functor for integer types
//
// See "Numeric" above for description of the generator functor
diff --git a/cpp/src/arrow/compute/kernels/scalar_boolean.cc b/cpp/src/arrow/compute/kernels/scalar_boolean.cc
index 3d47d23..8910712 100644
--- a/cpp/src/arrow/compute/kernels/scalar_boolean.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_boolean.cc
@@ -95,7 +95,7 @@ inline Bitmap GetBitmap(const ArrayData& arr, int index) {
return Bitmap{arr.buffers[index], arr.offset, arr.length};
}
-struct Invert {
+struct InvertOp {
static Status Call(KernelContext* ctx, const Scalar& in, Scalar* out) {
*checked_cast<BooleanScalar*>(out) = InvertScalar(in);
return Status::OK();
@@ -115,8 +115,8 @@ struct Commutative {
}
};
-struct And : Commutative<And> {
- using Commutative<And>::Call;
+struct AndOp : Commutative<AndOp> {
+ using Commutative<AndOp>::Call;
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
@@ -147,8 +147,8 @@ struct And : Commutative<And> {
}
};
-struct KleeneAnd : Commutative<KleeneAnd> {
- using Commutative<KleeneAnd>::Call;
+struct KleeneAndOp : Commutative<KleeneAndOp> {
+ using Commutative<KleeneAndOp>::Call;
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
@@ -205,7 +205,7 @@ struct KleeneAnd : Commutative<KleeneAnd> {
if (left.GetNullCount() == 0 && right.GetNullCount() == 0) {
out->null_count = 0;
out->buffers[0] = nullptr;
- return And::Call(ctx, left, right, out);
+ return AndOp::Call(ctx, left, right, out);
}
auto compute_word = [](uint64_t left_true, uint64_t left_false, uint64_t right_true,
uint64_t right_false, uint64_t* out_valid,
@@ -218,8 +218,8 @@ struct KleeneAnd : Commutative<KleeneAnd> {
}
};
-struct Or : Commutative<Or> {
- using Commutative<Or>::Call;
+struct OrOp : Commutative<OrOp> {
+ using Commutative<OrOp>::Call;
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
@@ -250,8 +250,8 @@ struct Or : Commutative<Or> {
}
};
-struct KleeneOr : Commutative<KleeneOr> {
- using Commutative<KleeneOr>::Call;
+struct KleeneOrOp : Commutative<KleeneOrOp> {
+ using Commutative<KleeneOrOp>::Call;
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
@@ -308,7 +308,7 @@ struct KleeneOr : Commutative<KleeneOr> {
if (left.GetNullCount() == 0 && right.GetNullCount() == 0) {
out->null_count = 0;
out->buffers[0] = nullptr;
- return Or::Call(ctx, left, right, out);
+ return OrOp::Call(ctx, left, right, out);
}
static auto compute_word = [](uint64_t left_true, uint64_t left_false,
@@ -323,8 +323,8 @@ struct KleeneOr : Commutative<KleeneOr> {
}
};
-struct Xor : Commutative<Xor> {
- using Commutative<Xor>::Call;
+struct XorOp : Commutative<XorOp> {
+ using Commutative<XorOp>::Call;
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
@@ -355,10 +355,10 @@ struct Xor : Commutative<Xor> {
}
};
-struct AndNot {
+struct AndNotOp {
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
- return And::Call(ctx, left, InvertScalar(right), out);
+ return AndOp::Call(ctx, left, InvertScalar(right), out);
}
static Status Call(KernelContext* ctx, const Scalar& left, const ArrayData& right,
@@ -373,7 +373,7 @@ struct AndNot {
static Status Call(KernelContext* ctx, const ArrayData& left, const Scalar& right,
ArrayData* out) {
- return And::Call(ctx, left, InvertScalar(right), out);
+ return AndOp::Call(ctx, left, InvertScalar(right), out);
}
static Status Call(KernelContext* ctx, const ArrayData& left, const ArrayData& right,
@@ -385,10 +385,10 @@ struct AndNot {
}
};
-struct KleeneAndNot {
+struct KleeneAndNotOp {
static Status Call(KernelContext* ctx, const Scalar& left, const Scalar& right,
Scalar* out) {
- return KleeneAnd::Call(ctx, left, InvertScalar(right), out);
+ return KleeneAndOp::Call(ctx, left, InvertScalar(right), out);
}
static Status Call(KernelContext* ctx, const Scalar& left, const ArrayData& right,
@@ -430,7 +430,7 @@ struct KleeneAndNot {
static Status Call(KernelContext* ctx, const ArrayData& left, const Scalar& right,
ArrayData* out) {
- return KleeneAnd::Call(ctx, left, InvertScalar(right), out);
+ return KleeneAndOp::Call(ctx, left, InvertScalar(right), out);
}
static Status Call(KernelContext* ctx, const ArrayData& left, const ArrayData& right,
@@ -438,7 +438,7 @@ struct KleeneAndNot {
if (left.GetNullCount() == 0 && right.GetNullCount() == 0) {
out->null_count = 0;
out->buffers[0] = nullptr;
- return AndNot::Call(ctx, left, right, out);
+ return AndNotOp::Call(ctx, left, right, out);
}
static auto compute_word = [](uint64_t left_true, uint64_t left_false,
@@ -543,20 +543,20 @@ namespace internal {
void RegisterScalarBoolean(FunctionRegistry* registry) {
// These functions can write into sliced output bitmaps
- MakeFunction("invert", 1, applicator::SimpleUnary<Invert>, &invert_doc, registry);
- MakeFunction("and", 2, applicator::SimpleBinary<And>, &and_doc, registry);
- MakeFunction("and_not", 2, applicator::SimpleBinary<AndNot>, &and_not_doc, registry);
- MakeFunction("or", 2, applicator::SimpleBinary<Or>, &or_doc, registry);
- MakeFunction("xor", 2, applicator::SimpleBinary<Xor>, &xor_doc, registry);
+ MakeFunction("invert", 1, applicator::SimpleUnary<InvertOp>, &invert_doc, registry);
+ MakeFunction("and", 2, applicator::SimpleBinary<AndOp>, &and_doc, registry);
+ MakeFunction("and_not", 2, applicator::SimpleBinary<AndNotOp>, &and_not_doc, registry);
+ MakeFunction("or", 2, applicator::SimpleBinary<OrOp>, &or_doc, registry);
+ MakeFunction("xor", 2, applicator::SimpleBinary<XorOp>, &xor_doc, registry);
// The Kleene logic kernels cannot write into sliced output bitmaps
- MakeFunction("and_kleene", 2, applicator::SimpleBinary<KleeneAnd>, &and_kleene_doc,
+ MakeFunction("and_kleene", 2, applicator::SimpleBinary<KleeneAndOp>, &and_kleene_doc,
registry,
/*can_write_into_slices=*/false, NullHandling::COMPUTED_PREALLOCATE);
- MakeFunction("and_not_kleene", 2, applicator::SimpleBinary<KleeneAndNot>,
+ MakeFunction("and_not_kleene", 2, applicator::SimpleBinary<KleeneAndNotOp>,
&and_not_kleene_doc, registry,
/*can_write_into_slices=*/false, NullHandling::COMPUTED_PREALLOCATE);
- MakeFunction("or_kleene", 2, applicator::SimpleBinary<KleeneOr>, &or_kleene_doc,
+ MakeFunction("or_kleene", 2, applicator::SimpleBinary<KleeneOrOp>, &or_kleene_doc,
registry,
/*can_write_into_slices=*/false, NullHandling::COMPUTED_PREALLOCATE);
}
diff --git a/cpp/src/arrow/compute/kernels/scalar_compare.cc b/cpp/src/arrow/compute/kernels/scalar_compare.cc
index 8da97ef..8e9e224 100644
--- a/cpp/src/arrow/compute/kernels/scalar_compare.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_compare.cc
@@ -15,7 +15,12 @@
// specific language governing permissions and limitations
// under the License.
+#include <cmath>
+#include <limits>
+
+#include "arrow/compute/api_scalar.h"
#include "arrow/compute/kernels/common.h"
+#include "arrow/util/bitmap_ops.h"
namespace arrow {
@@ -56,6 +61,75 @@ struct GreaterEqual {
}
};
+template <typename T>
+using is_unsigned_integer = std::integral_constant<bool, std::is_integral<T>::value &&
+ std::is_unsigned<T>::value>;
+
+template <typename T>
+using is_signed_integer =
+ std::integral_constant<bool, std::is_integral<T>::value && std::is_signed<T>::value>;
+
+template <typename T>
+using enable_if_integer =
+ enable_if_t<is_signed_integer<T>::value || is_unsigned_integer<T>::value, T>;
+
+template <typename T>
+using enable_if_floating_point = enable_if_t<std::is_floating_point<T>::value, T>;
+
+struct Minimum {
+ template <typename T>
+ static enable_if_floating_point<T> Call(T left, T right) {
+ return std::fmin(left, right);
+ }
+
+ template <typename T>
+ static enable_if_integer<T> Call(T left, T right) {
+ return std::min(left, right);
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_same<float, T>::value, T> antiextreme() {
+ return std::nanf("");
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_same<double, T>::value, T> antiextreme() {
+ return std::nan("");
+ }
+
+ template <typename T>
+ static constexpr enable_if_integer<T> antiextreme() {
+ return std::numeric_limits<T>::max();
+ }
+};
+
+struct Maximum {
+ template <typename T>
+ static enable_if_floating_point<T> Call(T left, T right) {
+ return std::fmax(left, right);
+ }
+
+ template <typename T>
+ static enable_if_integer<T> Call(T left, T right) {
+ return std::max(left, right);
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_same<float, T>::value, T> antiextreme() {
+ return std::nanf("");
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_same<double, T>::value, T> antiextreme() {
+ return std::nan("");
+ }
+
+ template <typename T>
+ static constexpr enable_if_integer<T> antiextreme() {
+ return std::numeric_limits<T>::min();
+ }
+};
+
// Implement Less, LessEqual by flipping arguments to Greater, GreaterEqual
template <typename Op>
@@ -97,6 +171,28 @@ struct CompareFunction : ScalarFunction {
}
};
+struct VarArgsCompareFunction : ScalarFunction {
+ using ScalarFunction::ScalarFunction;
+
+ Result<const Kernel*> DispatchBest(std::vector<ValueDescr>* values) const override {
+ RETURN_NOT_OK(CheckArity(*values));
+
+ using arrow::compute::detail::DispatchExactImpl;
+ if (auto kernel = DispatchExactImpl(this, *values)) return kernel;
+
+ EnsureDictionaryDecoded(values);
+
+ if (auto type = CommonNumeric(*values)) {
+ ReplaceTypes(type, values);
+ } else if (auto type = CommonTimestamp(*values)) {
+ ReplaceTypes(type, values);
+ }
+
+ if (auto kernel = DispatchExactImpl(this, *values)) return kernel;
+ return arrow::compute::detail::NoMatchingKernel(this, *values);
+ }
+};
+
template <typename Op>
std::shared_ptr<ScalarFunction> MakeCompareFunction(std::string name,
const FunctionDoc* doc) {
@@ -170,6 +266,177 @@ std::shared_ptr<ScalarFunction> MakeFlippedFunction(std::string name,
return flipped_func;
}
+using MinMaxState = OptionsWrapper<ElementWiseAggregateOptions>;
+
+// Implement a variadic scalar min/max kernel.
+template <typename OutType, typename Op>
+struct ScalarMinMax {
+ using OutValue = typename GetOutputType<OutType>::T;
+
+ static void ExecScalar(const ExecBatch& batch,
+ const ElementWiseAggregateOptions& options, Scalar* out) {
+ // All arguments are scalar
+ OutValue value{};
+ bool valid = false;
+ for (const auto& arg : batch.values) {
+ // Ignore non-scalar arguments so we can use it in the mixed-scalar-and-array case
+ if (!arg.is_scalar()) continue;
+ const auto& scalar = *arg.scalar();
+ if (!scalar.is_valid) {
+ if (options.skip_nulls) continue;
+ out->is_valid = false;
+ return;
+ }
+ if (!valid) {
+ value = UnboxScalar<OutType>::Unbox(scalar);
+ valid = true;
+ } else {
+ value = Op::Call(value, UnboxScalar<OutType>::Unbox(scalar));
+ }
+ }
+ out->is_valid = valid;
+ if (valid) {
+ BoxScalar<OutType>::Box(value, out);
+ }
+ }
+
+ static Status Exec(KernelContext* ctx, const ExecBatch& batch, Datum* out) {
+ const ElementWiseAggregateOptions& options = MinMaxState::Get(ctx);
+ const auto descrs = batch.GetDescriptors();
+ const size_t scalar_count =
+ static_cast<size_t>(std::count_if(batch.values.begin(), batch.values.end(),
+ [](const Datum& d) { return d.is_scalar(); }));
+ if (scalar_count == batch.values.size()) {
+ ExecScalar(batch, options, out->scalar().get());
+ return Status::OK();
+ }
+
+ ArrayData* output = out->mutable_array();
+
+ // At least one array, two or more arguments
+ ArrayDataVector arrays;
+ for (const auto& arg : batch.values) {
+ if (!arg.is_array()) continue;
+ arrays.push_back(arg.array());
+ }
+
+ bool initialize_output = true;
+ if (scalar_count > 0) {
+ ARROW_ASSIGN_OR_RAISE(std::shared_ptr<Scalar> temp_scalar,
+ MakeScalar(out->type(), 0));
+ ExecScalar(batch, options, temp_scalar.get());
+ if (temp_scalar->is_valid) {
+ const auto value = UnboxScalar<OutType>::Unbox(*temp_scalar);
+ initialize_output = false;
+ OutValue* out = output->GetMutableValues<OutValue>(1);
+ std::fill(out, out + batch.length, value);
+ } else if (!options.skip_nulls) {
+ // Abort early
+ ARROW_ASSIGN_OR_RAISE(auto array, MakeArrayFromScalar(*temp_scalar, batch.length,
+ ctx->memory_pool()));
+ *output = *array->data();
+ return Status::OK();
+ }
+ }
+
+ if (initialize_output) {
+ OutValue* out = output->GetMutableValues<OutValue>(1);
+ std::fill(out, out + batch.length, Op::template antiextreme<OutValue>());
+ }
+
+ // Precompute the validity buffer
+ if (options.skip_nulls && initialize_output) {
+ // OR together the validity buffers of all arrays
+ if (std::all_of(arrays.begin(), arrays.end(),
+ [](const std::shared_ptr<ArrayData>& arr) {
+ return arr->MayHaveNulls();
+ })) {
+ for (const auto& arr : arrays) {
+ if (!arr->MayHaveNulls()) continue;
+ if (!output->buffers[0]) {
+ ARROW_ASSIGN_OR_RAISE(output->buffers[0], ctx->AllocateBitmap(batch.length));
+ ::arrow::internal::CopyBitmap(arr->buffers[0]->data(), arr->offset,
+
+ batch.length,
+ output->buffers[0]->mutable_data(),
+ /*dest_offset=*/0);
+ } else {
+ ::arrow::internal::BitmapOr(
+ output->buffers[0]->data(), /*left_offset=*/0, arr->buffers[0]->data(),
+ arr->offset, batch.length,
+ /*out_offset=*/0, output->buffers[0]->mutable_data());
+ }
+ }
+ }
+ } else if (!options.skip_nulls) {
+ // AND together the validity buffers of all arrays
+ for (const auto& arr : arrays) {
+ if (!arr->MayHaveNulls()) continue;
+ if (!output->buffers[0]) {
+ ARROW_ASSIGN_OR_RAISE(output->buffers[0], ctx->AllocateBitmap(batch.length));
+ ::arrow::internal::CopyBitmap(arr->buffers[0]->data(), arr->offset,
+ batch.length, output->buffers[0]->mutable_data(),
+ /*dest_offset=*/0);
+ } else {
+ ::arrow::internal::BitmapAnd(output->buffers[0]->data(), /*left_offset=*/0,
+ arr->buffers[0]->data(), arr->offset, batch.length,
+ /*out_offset=*/0,
+ output->buffers[0]->mutable_data());
+ }
+ }
+ }
+
+ for (const auto& array : arrays) {
+ OutputArrayWriter<OutType> writer(out->mutable_array());
+ ArrayIterator<OutType> out_it(*output);
+ int64_t index = 0;
+ VisitArrayValuesInline<OutType>(
+ *array,
+ [&](OutValue value) {
+ auto u = out_it();
+ if (!output->buffers[0] ||
+ BitUtil::GetBit(output->buffers[0]->data(), index)) {
+ writer.Write(Op::Call(u, value));
+ } else {
+ writer.Write(value);
+ }
+ index++;
+ },
+ [&]() {
+ // RHS is null, preserve the LHS
+ writer.values++;
+ index++;
+ out_it();
+ });
+ }
+ output->null_count = output->buffers[0] ? -1 : 0;
+ return Status::OK();
+ }
+};
+
+template <typename Op>
+std::shared_ptr<ScalarFunction> MakeScalarMinMax(std::string name,
+ const FunctionDoc* doc) {
+ auto func = std::make_shared<VarArgsCompareFunction>(name, Arity::VarArgs(), doc);
+ for (const auto& ty : NumericTypes()) {
+ auto exec = GeneratePhysicalNumeric<ScalarMinMax, Op>(ty);
+ ScalarKernel kernel{KernelSignature::Make({ty}, ty, /*is_varargs=*/true), exec,
+ MinMaxState::Init};
+ kernel.null_handling = NullHandling::type::COMPUTED_NO_PREALLOCATE;
+ kernel.mem_allocation = MemAllocation::type::PREALLOCATE;
+ DCHECK_OK(func->AddKernel(std::move(kernel)));
+ }
+ for (const auto& ty : TemporalTypes()) {
+ auto exec = GeneratePhysicalNumeric<ScalarMinMax, Op>(ty);
+ ScalarKernel kernel{KernelSignature::Make({ty}, ty, /*is_varargs=*/true), exec,
+ MinMaxState::Init};
+ kernel.null_handling = NullHandling::type::COMPUTED_NO_PREALLOCATE;
+ kernel.mem_allocation = MemAllocation::type::PREALLOCATE;
+ DCHECK_OK(func->AddKernel(std::move(kernel)));
+ }
+ return func;
+}
+
const FunctionDoc equal_doc{"Compare values for equality (x == y)",
("A null on either side emits a null comparison result."),
{"x", "y"}};
@@ -196,6 +463,19 @@ const FunctionDoc less_equal_doc{
("A null on either side emits a null comparison result."),
{"x", "y"}};
+const FunctionDoc element_wise_min_doc{
+ "Find the element-wise minimum value",
+ ("Nulls will be ignored (default) or propagated. "
+ "NaN will be taken over null, but not over any valid float."),
+ {"*args"},
+ "ElementWiseAggregateOptions"};
+
+const FunctionDoc element_wise_max_doc{
+ "Find the element-wise maximum value",
+ ("Nulls will be ignored (default) or propagated. "
+ "NaN will be taken over null, but not over any valid float."),
+ {"*args"},
+ "ElementWiseAggregateOptions"};
} // namespace
void RegisterScalarComparison(FunctionRegistry* registry) {
@@ -213,6 +493,17 @@ void RegisterScalarComparison(FunctionRegistry* registry) {
DCHECK_OK(registry->AddFunction(std::move(less_equal)));
DCHECK_OK(registry->AddFunction(std::move(greater)));
DCHECK_OK(registry->AddFunction(std::move(greater_equal)));
+
+ // ----------------------------------------------------------------------
+ // Variadic element-wise functions
+
+ auto element_wise_min =
+ MakeScalarMinMax<Minimum>("element_wise_min", &element_wise_min_doc);
+ DCHECK_OK(registry->AddFunction(std::move(element_wise_min)));
+
+ auto element_wise_max =
+ MakeScalarMinMax<Maximum>("element_wise_max", &element_wise_max_doc);
+ DCHECK_OK(registry->AddFunction(std::move(element_wise_max)));
}
} // namespace internal
diff --git a/cpp/src/arrow/compute/kernels/scalar_compare_test.cc b/cpp/src/arrow/compute/kernels/scalar_compare_test.cc
index 7b09063..6318a89 100644
--- a/cpp/src/arrow/compute/kernels/scalar_compare_test.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_compare_test.cc
@@ -652,5 +652,353 @@ TEST_F(TestStringCompareKernel, RandomCompareArrayArray) {
}
}
+template <typename T>
+class TestVarArgsCompare : public TestBase {
+ protected:
+ static std::shared_ptr<DataType> type_singleton() {
+ return TypeTraits<T>::type_singleton();
+ }
+
+ using VarArgsFunction = std::function<Result<Datum>(
+ const std::vector<Datum>&, ElementWiseAggregateOptions, ExecContext*)>;
+
+ void SetUp() override { equal_options_ = equal_options_.nans_equal(true); }
+
+ Datum scalar(const std::string& value) {
+ return ScalarFromJSON(type_singleton(), value);
+ }
+
+ Datum array(const std::string& value) { return ArrayFromJSON(type_singleton(), value); }
+
+ Datum Eval(VarArgsFunction func, const std::vector<Datum>& args) {
+ EXPECT_OK_AND_ASSIGN(auto actual,
+ func(args, element_wise_aggregate_options_, nullptr));
+ if (actual.is_array()) {
+ auto arr = actual.make_array();
+ ARROW_EXPECT_OK(arr->ValidateFull());
+ }
+ return actual;
+ }
+
+ void AssertNullScalar(VarArgsFunction func, const std::vector<Datum>& args) {
+ auto datum = this->Eval(func, args);
+ ASSERT_TRUE(datum.is_scalar());
+ ASSERT_FALSE(datum.scalar()->is_valid);
+ }
+
+ void Assert(VarArgsFunction func, Datum expected, const std::vector<Datum>& args) {
+ auto actual = Eval(func, args);
+ AssertDatumsApproxEqual(expected, actual, /*verbose=*/true, equal_options_);
+ }
+
+ EqualOptions equal_options_ = EqualOptions::Defaults();
+ ElementWiseAggregateOptions element_wise_aggregate_options_;
+};
+
+template <typename T>
+class TestVarArgsCompareNumeric : public TestVarArgsCompare<T> {};
+
+template <typename T>
+class TestVarArgsCompareFloating : public TestVarArgsCompare<T> {};
+
+template <typename T>
+class TestVarArgsCompareParametricTemporal : public TestVarArgsCompare<T> {
+ protected:
+ static std::shared_ptr<DataType> type_singleton() {
+ // Time32 requires second/milli, Time64 requires nano/micro
+ if (TypeTraits<T>::bytes_required(1) == 4) {
+ return std::make_shared<T>(TimeUnit::type::SECOND);
+ } else {
+ return std::make_shared<T>(TimeUnit::type::NANO);
+ }
+ }
+
+ Datum scalar(const std::string& value) {
+ return ScalarFromJSON(type_singleton(), value);
+ }
+
+ Datum array(const std::string& value) { return ArrayFromJSON(type_singleton(), value); }
+};
+
+using NumericBasedTypes =
+ ::testing::Types<UInt8Type, UInt16Type, UInt32Type, UInt64Type, Int8Type, Int16Type,
+ Int32Type, Int64Type, FloatType, DoubleType, Date32Type, Date64Type>;
+using ParametricTemporalTypes = ::testing::Types<TimestampType, Time32Type, Time64Type>;
+
+TYPED_TEST_SUITE(TestVarArgsCompareNumeric, NumericBasedTypes);
+TYPED_TEST_SUITE(TestVarArgsCompareFloating, RealArrowTypes);
+TYPED_TEST_SUITE(TestVarArgsCompareParametricTemporal, ParametricTemporalTypes);
+
+TYPED_TEST(TestVarArgsCompareNumeric, ElementWiseMin) {
+ this->AssertNullScalar(ElementWiseMin, {});
+ this->AssertNullScalar(ElementWiseMin, {this->scalar("null"), this->scalar("null")});
+
+ this->Assert(ElementWiseMin, this->scalar("0"), {this->scalar("0")});
+ this->Assert(ElementWiseMin, this->scalar("0"),
+ {this->scalar("2"), this->scalar("0"), this->scalar("1")});
+ this->Assert(
+ ElementWiseMin, this->scalar("0"),
+ {this->scalar("2"), this->scalar("0"), this->scalar("1"), this->scalar("null")});
+ this->Assert(ElementWiseMin, this->scalar("1"),
+ {this->scalar("null"), this->scalar("null"), this->scalar("1"),
+ this->scalar("null")});
+
+ this->Assert(ElementWiseMin, (this->array("[]")), {this->array("[]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 3, null]"),
+ {this->array("[1, 2, 3, null]")});
+
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, 2, 3, 4]"), this->scalar("2")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("2")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("2"), this->scalar("4")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("null"), this->scalar("2")});
+
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[2, 2, 2, 2]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[2, null, 2, 2]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, null, 3, 4]"), this->array("[2, 2, 2, 2]")});
+
+ this->Assert(ElementWiseMin, this->array("[1, 2, null, 6]"),
+ {this->array("[1, 2, null, null]"), this->array("[4, null, null, 6]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, null, 6]"),
+ {this->array("[4, null, null, 6]"), this->array("[1, 2, null, null]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 3, 4]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[null, null, null, null]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 3, 4]"),
+ {this->array("[null, null, null, null]"), this->array("[1, 2, 3, 4]")});
+
+ this->Assert(ElementWiseMin, this->array("[1, 1, 1, 1]"),
+ {this->scalar("1"), this->array("[1, 2, 3, 4]")});
+ this->Assert(ElementWiseMin, this->array("[1, 1, 1, 1]"),
+ {this->scalar("1"), this->array("[null, null, null, null]")});
+ this->Assert(ElementWiseMin, this->array("[1, 1, 1, 1]"),
+ {this->scalar("null"), this->array("[1, 1, 1, 1]")});
+ this->Assert(ElementWiseMin, this->array("[null, null, null, null]"),
+ {this->scalar("null"), this->array("[null, null, null, null]")});
+
+ // Test null handling
+ this->element_wise_aggregate_options_.skip_nulls = false;
+ this->AssertNullScalar(ElementWiseMin, {this->scalar("null"), this->scalar("null")});
+ this->AssertNullScalar(ElementWiseMin, {this->scalar("0"), this->scalar("null")});
+
+ this->Assert(ElementWiseMin, this->array("[1, null, 2, 2]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("2"), this->scalar("4")});
+ this->Assert(ElementWiseMin, this->array("[null, null, null, null]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("null"), this->scalar("2")});
+ this->Assert(ElementWiseMin, this->array("[1, null, 2, 2]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[2, null, 2, 2]")});
+
+ this->Assert(ElementWiseMin, this->array("[null, null, null, null]"),
+ {this->scalar("1"), this->array("[null, null, null, null]")});
+ this->Assert(ElementWiseMin, this->array("[null, null, null, null]"),
+ {this->scalar("null"), this->array("[1, 1, 1, 1]")});
+}
+
+TYPED_TEST(TestVarArgsCompareFloating, ElementWiseMin) {
+ auto Check = [this](const std::string& expected,
+ const std::vector<std::string>& inputs) {
+ std::vector<Datum> args;
+ for (const auto& input : inputs) {
+ args.emplace_back(this->scalar(input));
+ }
+ this->Assert(ElementWiseMin, this->scalar(expected), args);
+
+ args.clear();
+ for (const auto& input : inputs) {
+ args.emplace_back(this->array("[" + input + "]"));
+ }
+ this->Assert(ElementWiseMin, this->array("[" + expected + "]"), args);
+ };
+ Check("-0.0", {"0.0", "-0.0"});
+ Check("-0.0", {"1.0", "-0.0", "0.0"});
+ Check("-1.0", {"-1.0", "-0.0"});
+ Check("0", {"0", "NaN"});
+ Check("0", {"NaN", "0"});
+ Check("Inf", {"Inf", "NaN"});
+ Check("Inf", {"NaN", "Inf"});
+ Check("-Inf", {"-Inf", "NaN"});
+ Check("-Inf", {"NaN", "-Inf"});
+ Check("NaN", {"NaN", "null"});
+ Check("0", {"0", "Inf"});
+ Check("-Inf", {"0", "-Inf"});
+}
+
+TYPED_TEST(TestVarArgsCompareParametricTemporal, ElementWiseMin) {
+ // Temporal kernel is implemented with numeric kernel underneath
+ this->AssertNullScalar(ElementWiseMin, {});
+ this->AssertNullScalar(ElementWiseMin, {this->scalar("null"), this->scalar("null")});
+
+ this->Assert(ElementWiseMin, this->scalar("0"), {this->scalar("0")});
+ this->Assert(ElementWiseMin, this->scalar("0"), {this->scalar("2"), this->scalar("0")});
+ this->Assert(ElementWiseMin, this->scalar("0"),
+ {this->scalar("0"), this->scalar("null")});
+
+ this->Assert(ElementWiseMin, (this->array("[]")), {this->array("[]")});
+ this->Assert(ElementWiseMin, this->array("[1, 2, 3, null]"),
+ {this->array("[1, 2, 3, null]")});
+
+ this->Assert(ElementWiseMin, this->array("[1, 2, 2, 2]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("null"), this->scalar("2")});
+
+ this->Assert(ElementWiseMin, this->array("[1, 2, 3, 2]"),
+ {this->array("[1, null, 3, 4]"), this->array("[2, 2, null, 2]")});
+}
+
+TYPED_TEST(TestVarArgsCompareNumeric, ElementWiseMax) {
+ this->AssertNullScalar(ElementWiseMax, {});
+ this->AssertNullScalar(ElementWiseMax, {this->scalar("null"), this->scalar("null")});
+
+ this->Assert(ElementWiseMax, this->scalar("0"), {this->scalar("0")});
+ this->Assert(ElementWiseMax, this->scalar("2"),
+ {this->scalar("2"), this->scalar("0"), this->scalar("1")});
+ this->Assert(
+ ElementWiseMax, this->scalar("2"),
+ {this->scalar("2"), this->scalar("0"), this->scalar("1"), this->scalar("null")});
+ this->Assert(ElementWiseMax, this->scalar("1"),
+ {this->scalar("null"), this->scalar("null"), this->scalar("1"),
+ this->scalar("null")});
+
+ this->Assert(ElementWiseMax, (this->array("[]")), {this->array("[]")});
+ this->Assert(ElementWiseMax, this->array("[1, 2, 3, null]"),
+ {this->array("[1, 2, 3, null]")});
+
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, 2, 3, 4]"), this->scalar("2")});
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("2")});
+ this->Assert(ElementWiseMax, this->array("[4, 4, 4, 4]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("2"), this->scalar("4")});
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("null"), this->scalar("2")});
+
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[2, 2, 2, 2]")});
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[2, null, 2, 2]")});
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, null, 3, 4]"), this->array("[2, 2, 2, 2]")});
+
+ this->Assert(ElementWiseMax, this->array("[4, 2, null, 6]"),
+ {this->array("[1, 2, null, null]"), this->array("[4, null, null, 6]")});
+ this->Assert(ElementWiseMax, this->array("[4, 2, null, 6]"),
+ {this->array("[4, null, null, 6]"), this->array("[1, 2, null, null]")});
+ this->Assert(ElementWiseMax, this->array("[1, 2, 3, 4]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[null, null, null, null]")});
+ this->Assert(ElementWiseMax, this->array("[1, 2, 3, 4]"),
+ {this->array("[null, null, null, null]"), this->array("[1, 2, 3, 4]")});
+
+ this->Assert(ElementWiseMax, this->array("[1, 2, 3, 4]"),
+ {this->scalar("1"), this->array("[1, 2, 3, 4]")});
+ this->Assert(ElementWiseMax, this->array("[1, 1, 1, 1]"),
+ {this->scalar("1"), this->array("[null, null, null, null]")});
+ this->Assert(ElementWiseMax, this->array("[1, 1, 1, 1]"),
+ {this->scalar("null"), this->array("[1, 1, 1, 1]")});
+ this->Assert(ElementWiseMax, this->array("[null, null, null, null]"),
+ {this->scalar("null"), this->array("[null, null, null, null]")});
+
+ // Test null handling
+ this->element_wise_aggregate_options_.skip_nulls = false;
+ this->AssertNullScalar(ElementWiseMax, {this->scalar("null"), this->scalar("null")});
+ this->AssertNullScalar(ElementWiseMax, {this->scalar("0"), this->scalar("null")});
+
+ this->Assert(ElementWiseMax, this->array("[4, null, 4, 4]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("2"), this->scalar("4")});
+ this->Assert(ElementWiseMax, this->array("[null, null, null, null]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("null"), this->scalar("2")});
+ this->Assert(ElementWiseMax, this->array("[2, null, 3, 4]"),
+ {this->array("[1, 2, 3, 4]"), this->array("[2, null, 2, 2]")});
+
+ this->Assert(ElementWiseMax, this->array("[null, null, null, null]"),
+ {this->scalar("1"), this->array("[null, null, null, null]")});
+ this->Assert(ElementWiseMax, this->array("[null, null, null, null]"),
+ {this->scalar("null"), this->array("[1, 1, 1, 1]")});
+}
+
+TYPED_TEST(TestVarArgsCompareFloating, ElementWiseMax) {
+ auto Check = [this](const std::string& expected,
+ const std::vector<std::string>& inputs) {
+ std::vector<Datum> args;
+ for (const auto& input : inputs) {
+ args.emplace_back(this->scalar(input));
+ }
+ this->Assert(ElementWiseMax, this->scalar(expected), args);
+
+ args.clear();
+ for (const auto& input : inputs) {
+ args.emplace_back(this->array("[" + input + "]"));
+ }
+ this->Assert(ElementWiseMax, this->array("[" + expected + "]"), args);
+ };
+ Check("0.0", {"0.0", "-0.0"});
+ Check("1.0", {"1.0", "-0.0", "0.0"});
+ Check("-0.0", {"-1.0", "-0.0"});
+ Check("0", {"0", "NaN"});
+ Check("0", {"NaN", "0"});
+ Check("Inf", {"Inf", "NaN"});
+ Check("Inf", {"NaN", "Inf"});
+ Check("-Inf", {"-Inf", "NaN"});
+ Check("-Inf", {"NaN", "-Inf"});
+ Check("NaN", {"NaN", "null"});
+ Check("Inf", {"0", "Inf"});
+ Check("0", {"0", "-Inf"});
+}
+
+TYPED_TEST(TestVarArgsCompareParametricTemporal, ElementWiseMax) {
+ // Temporal kernel is implemented with numeric kernel underneath
+ this->AssertNullScalar(ElementWiseMax, {});
+ this->AssertNullScalar(ElementWiseMax, {this->scalar("null"), this->scalar("null")});
+
+ this->Assert(ElementWiseMax, this->scalar("0"), {this->scalar("0")});
+ this->Assert(ElementWiseMax, this->scalar("2"), {this->scalar("2"), this->scalar("0")});
+ this->Assert(ElementWiseMax, this->scalar("0"),
+ {this->scalar("0"), this->scalar("null")});
+
+ this->Assert(ElementWiseMax, (this->array("[]")), {this->array("[]")});
+ this->Assert(ElementWiseMax, this->array("[1, 2, 3, null]"),
+ {this->array("[1, 2, 3, null]")});
+
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, null, 3, 4]"), this->scalar("null"), this->scalar("2")});
+
+ this->Assert(ElementWiseMax, this->array("[2, 2, 3, 4]"),
+ {this->array("[1, null, 3, 4]"), this->array("[2, 2, null, 2]")});
+}
+
+TEST(TestElementWiseMaxElementWiseMin, CommonTimestamp) {
+ {
+ auto t1 = std::make_shared<TimestampType>(TimeUnit::SECOND);
+ auto t2 = std::make_shared<TimestampType>(TimeUnit::MILLI);
+ auto expected = MakeScalar(t2, 1000).ValueOrDie();
+ ASSERT_OK_AND_ASSIGN(auto actual,
+ ElementWiseMin({Datum(MakeScalar(t1, 1).ValueOrDie()),
+ Datum(MakeScalar(t2, 12000).ValueOrDie())}));
+ AssertScalarsEqual(*expected, *actual.scalar(), /*verbose=*/true);
+ }
+ {
+ auto t1 = std::make_shared<Date32Type>();
+ auto t2 = std::make_shared<TimestampType>(TimeUnit::SECOND);
+ auto expected = MakeScalar(t2, 86401).ValueOrDie();
+ ASSERT_OK_AND_ASSIGN(auto actual,
+ ElementWiseMax({Datum(MakeScalar(t1, 1).ValueOrDie()),
+ Datum(MakeScalar(t2, 86401).ValueOrDie())}));
+ AssertScalarsEqual(*expected, *actual.scalar(), /*verbose=*/true);
+ }
+ {
+ auto t1 = std::make_shared<Date32Type>();
+ auto t2 = std::make_shared<Date64Type>();
+ auto t3 = std::make_shared<TimestampType>(TimeUnit::SECOND);
+ auto expected = MakeScalar(t3, 86400).ValueOrDie();
+ ASSERT_OK_AND_ASSIGN(
+ auto actual, ElementWiseMin({Datum(MakeScalar(t1, 1).ValueOrDie()),
+ Datum(MakeScalar(t2, 2 * 86400000).ValueOrDie())}));
+ AssertScalarsEqual(*expected, *actual.scalar(), /*verbose=*/true);
+ }
+}
+
} // namespace compute
} // namespace arrow
diff --git a/docs/source/cpp/compute.rst b/docs/source/cpp/compute.rst
index 4e729b0..0b54cd3 100644
--- a/docs/source/cpp/compute.rst
+++ b/docs/source/cpp/compute.rst
@@ -310,6 +310,21 @@ output element is null.
| less, less_equal | | | |
+--------------------------+------------+---------------------------------------------+---------------------+
+These functions take any number of inputs of numeric type (in which case they
+will be cast to the :ref:`common numeric type <common-numeric-type>` before
+comparison) or of temporal types. If any input is dictionary encoded it will be
+expanded for the purposes of comparison.
+
++--------------------------+------------+---------------------------------------------+---------------------+---------------------------------------+-------+
+| Function names | Arity | Input types | Output type | Options class | Notes |
++==========================+============+=============================================+=====================+=======================================+=======+
+| element_wise_max, | Varargs | Numeric and Temporal | Numeric or Temporal | :struct:`ElementWiseAggregateOptions` | \(1) |
+| element_wise_min | | | | | |
++--------------------------+------------+---------------------------------------------+---------------------+---------------------------------------+-------+
+
+* \(1) By default, nulls are skipped (but the kernel can be configured to propagate nulls).
+ For floating point values, NaN will be taken over null but not over any other value.
+
Logical functions
~~~~~~~~~~~~~~~~~~
diff --git a/docs/source/python/api/compute.rst b/docs/source/python/api/compute.rst
index 3010776..ccd5300 100644
--- a/docs/source/python/api/compute.rst
+++ b/docs/source/python/api/compute.rst
@@ -75,6 +75,14 @@ they return ``null``.
less_equal
not_equal
+These functions take any number of arguments of a numeric or temporal type.
+
+.. autosummary::
+ :toctree: ../generated/
+
+ element_wise_max
+ element_wise_min
+
Logical Functions
-----------------