pitrou commented on a change in pull request #10349:
URL: https://github.com/apache/arrow/pull/10349#discussion_r705326962
##########
File path: cpp/src/arrow/compute/api_scalar.h
##########
@@ -49,10 +49,58 @@ class ARROW_EXPORT ElementWiseAggregateOptions : public
FunctionOptions {
explicit ElementWiseAggregateOptions(bool skip_nulls = true);
constexpr static char const kTypeName[] = "ElementWiseAggregateOptions";
static ElementWiseAggregateOptions Defaults() { return
ElementWiseAggregateOptions{}; }
-
bool skip_nulls;
};
+/// Rounding and tie-breaking modes for round compute functions.
+/// Additional details and examples are provided in compute.rst.
+enum class RoundMode : int8_t {
+ /// Round to nearest integer less than or equal in magnitude (aka "floor")
+ DOWN,
+ /// Round to nearest integer greater than or equal in magnitude (aka "ceil")
+ UP,
+ /// Get the integral part without fractional digits (aka "trunc")
+ TOWARDS_ZERO,
+ /// Round negative values with DOWN rule and positive values with UP rule
+ TOWARDS_INFINITY,
+ /// Round ties with DOWN rule
+ HALF_DOWN,
+ /// Round ties with UP rule
+ HALF_UP,
+ /// Round ties with TOWARDS_ZERO rule
+ HALF_TOWARDS_ZERO,
+ /// Round ties with TOWARDS_INFINITY rule
+ HALF_TOWARDS_INFINITY,
+ /// Round ties to nearest even integer
+ HALF_TO_EVEN,
+ /// Round ties to nearest odd integer
+ HALF_TO_ODD,
+};
+
+class ARROW_EXPORT RoundOptions : public FunctionOptions {
+ public:
+ explicit RoundOptions(int64_t ndigits = 0,
+ RoundMode round_mode = RoundMode::HALF_TO_EVEN);
+ constexpr static char const kTypeName[] = "RoundOptions";
+ static RoundOptions Defaults() { return RoundOptions(); }
+ /// Rounding precision (number of digits to round to).
+ int64_t ndigits;
+ /// Rounding and tie-breaking mode
+ RoundMode round_mode;
+};
+
+class ARROW_EXPORT RoundToMultipleOptions : public FunctionOptions {
+ public:
+ explicit RoundToMultipleOptions(double multiple = 1.0,
+ RoundMode round_mode =
RoundMode::HALF_TO_EVEN);
+ constexpr static char const kTypeName[] = "RoundToMultipleOptions";
+ static RoundToMultipleOptions Defaults() { return RoundToMultipleOptions(); }
+ /// Rounding scale (multiple to round to, only the absolute value is used).
Review comment:
Can remove the comment about the absolute value being used, since
negative values are disallowed.
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
##########
@@ -852,24 +853,243 @@ struct LogbChecked {
}
};
+struct RoundUtil {
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsInteger(
+ const T val) {
+ // |frac| ~ 0.0?
+ return std::floor(val) == val;
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsHalfInteger(
+ const T val) {
+ // |frac| ~ 0.5?
+ return (val - std::floor(val)) == T(0.5);
+ }
+
+ // Calculate powers of ten with arbitrary integer exponent
+ template <typename T = double>
+ static enable_if_floating_point<T> Pow10(int64_t power) {
+ static constexpr T lut[] = {1e0F, 1e1F, 1e2F, 1e3F, 1e4F, 1e5F, 1e6F,
1e7F,
+ 1e8F, 1e9F, 1e10F, 1e11F, 1e12F, 1e13F, 1e14F,
1e15F};
+ int64_t lut_size = (sizeof(lut) / sizeof(*lut));
+ int64_t abs_power = std::abs(power);
+ auto pow10 = lut[std::min(abs_power, lut_size - 1)];
+ while (abs_power-- >= lut_size) {
+ pow10 *= 1e1F;
+ }
+ return (power >= 0) ? pow10 : (1 / pow10);
+ }
+};
+
+// Specializations of rounding implementations for round kernels
+template <typename, RoundMode>
+struct RoundImpl;
+
+template <typename T>
+struct RoundImpl<T, RoundMode::DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::trunc(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::signbit(val) ? std::floor(val) : std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::DOWN>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::UP>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_ZERO>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_INFINITY>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_EVEN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::round(val * T(0.5)) * 2;
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_ODD> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val * T(0.5)) + std::ceil(val * T(0.5));
+ }
+};
+
+// Specializations of kernel state for round kernels
+template <typename>
+struct RoundOptionsWrapper;
+
+template <>
+struct RoundOptionsWrapper<RoundOptions> : public OptionsWrapper<RoundOptions>
{
+ using OptionsType = RoundOptions;
+ double pow10;
+
+ explicit RoundOptionsWrapper(OptionsType options) :
OptionsWrapper(std::move(options)) {
+ // Only positive of powers of 10 are used because combining multiply and
+ // division operations produced more stable rounding than using
multiply-only.
+ // Refer to NumPy's round implementation:
+ //
https://github.com/numpy/numpy/blob/7b2f20b406d27364c812f7a81a9c901afbd3600c/numpy/core/src/multiarray/calculation.c#L589
+ pow10 = RoundUtil::Pow10(std::abs(options.ndigits));
+ }
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ if (auto options = static_cast<const OptionsType*>(args.options)) {
+ return
arrow::internal::make_unique<RoundOptionsWrapper<OptionsType>>(*options);
+ }
+
+ return Status::Invalid(
+ "Attempted to initialize KernelState from null FunctionOptions");
+ }
+};
+
+template <>
+struct RoundOptionsWrapper<RoundToMultipleOptions>
+ : public OptionsWrapper<RoundToMultipleOptions> {
+ using OptionsType = RoundToMultipleOptions;
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ ARROW_ASSIGN_OR_RAISE(auto state, OptionsWrapper<OptionsType>::Init(ctx,
args));
+ auto options = Get(*state);
+ if (options.multiple <= 0) {
+ return Status::Invalid("Rounding multiple has to be a non-zero positive
value");
+ }
+ return std::move(state);
+ }
+};
+
+template <RoundMode RndMode>
+struct Round {
+ using State = RoundOptionsWrapper<RoundOptions>;
+
+ template <typename T, typename Arg>
+ static enable_if_floating_point<Arg, T> Call(KernelContext* ctx, Arg arg,
Status* st) {
+ static_assert(std::is_same<T, Arg>::value, "");
+ // Do not process Inf or NaN because they will trigger the overflow error
at end of
+ // function.
+ if (!std::isfinite(arg)) {
+ return arg;
+ }
+ auto state = static_cast<State*>(ctx->state());
+ auto options = state->options;
+ auto pow10 = T(state->pow10);
+ auto round_val = (options.ndigits >= 0) ? (arg * pow10) : (arg / pow10);
+ // Use std::round() if in tie-breaking mode and scaled value is not 0.5.
+ if ((options.round_mode >= RoundMode::HALF_DOWN) &&
+ !RoundUtil::IsHalfInteger(round_val)) {
+ round_val = std::round(round_val);
+ } else if (!RoundUtil::IsInteger(round_val)) {
Review comment:
I wonder: is this condition still useful? If `std::floor(round_val) ==
round_val`, then presumably the same also holds for `std::ceil` and
`std::trunc`, so `RoundImpl::Round` will be a no-op.
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
##########
@@ -852,24 +853,243 @@ struct LogbChecked {
}
};
+struct RoundUtil {
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsInteger(
+ const T val) {
+ // |frac| ~ 0.0?
+ return std::floor(val) == val;
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsHalfInteger(
+ const T val) {
+ // |frac| ~ 0.5?
+ return (val - std::floor(val)) == T(0.5);
+ }
+
+ // Calculate powers of ten with arbitrary integer exponent
+ template <typename T = double>
+ static enable_if_floating_point<T> Pow10(int64_t power) {
+ static constexpr T lut[] = {1e0F, 1e1F, 1e2F, 1e3F, 1e4F, 1e5F, 1e6F,
1e7F,
+ 1e8F, 1e9F, 1e10F, 1e11F, 1e12F, 1e13F, 1e14F,
1e15F};
+ int64_t lut_size = (sizeof(lut) / sizeof(*lut));
+ int64_t abs_power = std::abs(power);
+ auto pow10 = lut[std::min(abs_power, lut_size - 1)];
+ while (abs_power-- >= lut_size) {
+ pow10 *= 1e1F;
+ }
+ return (power >= 0) ? pow10 : (1 / pow10);
+ }
+};
+
+// Specializations of rounding implementations for round kernels
+template <typename, RoundMode>
+struct RoundImpl;
+
+template <typename T>
+struct RoundImpl<T, RoundMode::DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::trunc(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::signbit(val) ? std::floor(val) : std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::DOWN>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::UP>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_ZERO>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_INFINITY>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_EVEN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::round(val * T(0.5)) * 2;
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_ODD> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val * T(0.5)) + std::ceil(val * T(0.5));
+ }
+};
+
+// Specializations of kernel state for round kernels
+template <typename>
+struct RoundOptionsWrapper;
+
+template <>
+struct RoundOptionsWrapper<RoundOptions> : public OptionsWrapper<RoundOptions>
{
+ using OptionsType = RoundOptions;
+ double pow10;
+
+ explicit RoundOptionsWrapper(OptionsType options) :
OptionsWrapper(std::move(options)) {
+ // Only positive of powers of 10 are used because combining multiply and
+ // division operations produced more stable rounding than using
multiply-only.
+ // Refer to NumPy's round implementation:
+ //
https://github.com/numpy/numpy/blob/7b2f20b406d27364c812f7a81a9c901afbd3600c/numpy/core/src/multiarray/calculation.c#L589
+ pow10 = RoundUtil::Pow10(std::abs(options.ndigits));
+ }
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ if (auto options = static_cast<const OptionsType*>(args.options)) {
+ return
arrow::internal::make_unique<RoundOptionsWrapper<OptionsType>>(*options);
+ }
+
+ return Status::Invalid(
+ "Attempted to initialize KernelState from null FunctionOptions");
+ }
+};
+
+template <>
+struct RoundOptionsWrapper<RoundToMultipleOptions>
+ : public OptionsWrapper<RoundToMultipleOptions> {
+ using OptionsType = RoundToMultipleOptions;
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ ARROW_ASSIGN_OR_RAISE(auto state, OptionsWrapper<OptionsType>::Init(ctx,
args));
+ auto options = Get(*state);
+ if (options.multiple <= 0) {
+ return Status::Invalid("Rounding multiple has to be a non-zero positive
value");
+ }
+ return std::move(state);
+ }
+};
+
+template <RoundMode RndMode>
+struct Round {
+ using State = RoundOptionsWrapper<RoundOptions>;
+
+ template <typename T, typename Arg>
+ static enable_if_floating_point<Arg, T> Call(KernelContext* ctx, Arg arg,
Status* st) {
+ static_assert(std::is_same<T, Arg>::value, "");
+ // Do not process Inf or NaN because they will trigger the overflow error
at end of
+ // function.
+ if (!std::isfinite(arg)) {
+ return arg;
+ }
+ auto state = static_cast<State*>(ctx->state());
+ auto options = state->options;
+ auto pow10 = T(state->pow10);
+ auto round_val = (options.ndigits >= 0) ? (arg * pow10) : (arg / pow10);
+ // Use std::round() if in tie-breaking mode and scaled value is not 0.5.
+ if ((options.round_mode >= RoundMode::HALF_DOWN) &&
Review comment:
Hmm... why don't you use `RndMode` here instead of branching dynamically
on `options.round_mode`?
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
##########
@@ -1276,6 +1496,65 @@ std::shared_ptr<ScalarFunction>
MakeUnaryArithmeticFunctionNotNull(
return func;
}
+// Generate a kernel given an arithmetic rounding functor
+template <template <RoundMode> class Op>
+ArrayKernelExec GenerateArithmeticRound(RoundMode rmode, detail::GetTypeId ty)
{
+ switch (rmode) {
+ case RoundMode::DOWN:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
Op<RoundMode::DOWN>>(ty);
+ case RoundMode::UP:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
Op<RoundMode::UP>>(ty);
+ case RoundMode::TOWARDS_ZERO:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+ Op<RoundMode::TOWARDS_ZERO>>(ty);
+ case RoundMode::TOWARDS_INFINITY:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+
Op<RoundMode::TOWARDS_INFINITY>>(ty);
+ case RoundMode::HALF_DOWN:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+ Op<RoundMode::HALF_DOWN>>(ty);
+ case RoundMode::HALF_UP:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
Op<RoundMode::HALF_UP>>(
+ ty);
+ case RoundMode::HALF_TOWARDS_ZERO:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+
Op<RoundMode::HALF_TOWARDS_ZERO>>(ty);
+ case RoundMode::HALF_TOWARDS_INFINITY:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+
Op<RoundMode::HALF_TOWARDS_INFINITY>>(ty);
+ case RoundMode::HALF_TO_EVEN:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+ Op<RoundMode::HALF_TO_EVEN>>(ty);
+ case RoundMode::HALF_TO_ODD:
+ return GenerateArithmeticFloatingPoint<ScalarUnaryNotNull,
+ Op<RoundMode::HALF_TO_ODD>>(ty);
+ default:
+ DCHECK(false);
+ return ExecFail;
+ }
+}
+
+// Like MakeUnaryArithmeticFunction, but for unary rounding functions that
control
+// kernel dispatch based on RoundMode, only on non-null output.
+template <template <RoundMode> class Op, typename OptionsType>
+std::shared_ptr<ScalarFunction> MakeUnaryRoundFunction(std::string name,
+ const FunctionDoc* doc)
{
+ using State = RoundOptionsWrapper<OptionsType>;
+
+ static const OptionsType kDefaultOptions = OptionsType::Defaults();
+ auto func = std::make_shared<ArithmeticFloatingPointFunction>(name,
Arity::Unary(), doc,
+
&kDefaultOptions);
+ for (const auto& ty : FloatingPointTypes()) {
+ auto exec = [&](KernelContext* ctx, const ExecBatch& batch, Datum* out) {
+ auto options = State::Get(ctx);
+ auto exec_ = GenerateArithmeticRound<Op>(options.round_mode, ty);
Review comment:
Just for the record, it feels a bit weird to call
`GenerateArithmeticRound` at kernel execution time. That said, a quick
benchmarking in Python shows there doesn't seem to be any large overhead:
```python
>>> import pyarrow as pa, pyarrow.compute as pc
>>> floor = pc.get_function("floor")
>>> round = pc.get_function("round")
>>> arr = pa.array([None], type=pa.float64())
>>> %timeit floor.call([arr])
2.57 µs ± 10.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> %timeit floor.call([arr])
2.58 µs ± 11.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> %timeit round.call([arr])
2.65 µs ± 10.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
>>> %timeit round.call([arr])
2.53 µs ± 12.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
##########
@@ -852,24 +853,243 @@ struct LogbChecked {
}
};
+struct RoundUtil {
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsInteger(
+ const T val) {
+ // |frac| ~ 0.0?
+ return std::floor(val) == val;
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsHalfInteger(
+ const T val) {
+ // |frac| ~ 0.5?
+ return (val - std::floor(val)) == T(0.5);
+ }
+
+ // Calculate powers of ten with arbitrary integer exponent
+ template <typename T = double>
+ static enable_if_floating_point<T> Pow10(int64_t power) {
+ static constexpr T lut[] = {1e0F, 1e1F, 1e2F, 1e3F, 1e4F, 1e5F, 1e6F,
1e7F,
+ 1e8F, 1e9F, 1e10F, 1e11F, 1e12F, 1e13F, 1e14F,
1e15F};
+ int64_t lut_size = (sizeof(lut) / sizeof(*lut));
+ int64_t abs_power = std::abs(power);
+ auto pow10 = lut[std::min(abs_power, lut_size - 1)];
+ while (abs_power-- >= lut_size) {
+ pow10 *= 1e1F;
+ }
+ return (power >= 0) ? pow10 : (1 / pow10);
+ }
+};
+
+// Specializations of rounding implementations for round kernels
+template <typename, RoundMode>
+struct RoundImpl;
+
+template <typename T>
+struct RoundImpl<T, RoundMode::DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::trunc(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::signbit(val) ? std::floor(val) : std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::DOWN>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::UP>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_ZERO>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_INFINITY>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_EVEN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::round(val * T(0.5)) * 2;
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_ODD> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val * T(0.5)) + std::ceil(val * T(0.5));
+ }
+};
+
+// Specializations of kernel state for round kernels
+template <typename>
+struct RoundOptionsWrapper;
+
+template <>
+struct RoundOptionsWrapper<RoundOptions> : public OptionsWrapper<RoundOptions>
{
+ using OptionsType = RoundOptions;
+ double pow10;
+
+ explicit RoundOptionsWrapper(OptionsType options) :
OptionsWrapper(std::move(options)) {
+ // Only positive of powers of 10 are used because combining multiply and
+ // division operations produced more stable rounding than using
multiply-only.
+ // Refer to NumPy's round implementation:
+ //
https://github.com/numpy/numpy/blob/7b2f20b406d27364c812f7a81a9c901afbd3600c/numpy/core/src/multiarray/calculation.c#L589
+ pow10 = RoundUtil::Pow10(std::abs(options.ndigits));
+ }
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ if (auto options = static_cast<const OptionsType*>(args.options)) {
+ return
arrow::internal::make_unique<RoundOptionsWrapper<OptionsType>>(*options);
+ }
+
+ return Status::Invalid(
+ "Attempted to initialize KernelState from null FunctionOptions");
+ }
+};
+
+template <>
+struct RoundOptionsWrapper<RoundToMultipleOptions>
+ : public OptionsWrapper<RoundToMultipleOptions> {
+ using OptionsType = RoundToMultipleOptions;
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ ARROW_ASSIGN_OR_RAISE(auto state, OptionsWrapper<OptionsType>::Init(ctx,
args));
+ auto options = Get(*state);
+ if (options.multiple <= 0) {
+ return Status::Invalid("Rounding multiple has to be a non-zero positive
value");
+ }
+ return std::move(state);
+ }
+};
+
+template <RoundMode RndMode>
+struct Round {
+ using State = RoundOptionsWrapper<RoundOptions>;
+
+ template <typename T, typename Arg>
+ static enable_if_floating_point<Arg, T> Call(KernelContext* ctx, Arg arg,
Status* st) {
+ static_assert(std::is_same<T, Arg>::value, "");
+ // Do not process Inf or NaN because they will trigger the overflow error
at end of
+ // function.
+ if (!std::isfinite(arg)) {
+ return arg;
+ }
+ auto state = static_cast<State*>(ctx->state());
+ auto options = state->options;
+ auto pow10 = T(state->pow10);
+ auto round_val = (options.ndigits >= 0) ? (arg * pow10) : (arg / pow10);
+ // Use std::round() if in tie-breaking mode and scaled value is not 0.5.
+ if ((options.round_mode >= RoundMode::HALF_DOWN) &&
+ !RoundUtil::IsHalfInteger(round_val)) {
+ round_val = std::round(round_val);
Review comment:
So this is called even if `IsInteger(round_val)` would be true?
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
##########
@@ -43,8 +42,20 @@
namespace arrow {
namespace compute {
-template <typename T>
-class TestUnaryArithmetic : public TestBase {
+// InputType - OutputType pairs
Review comment:
I don't see any pairs here?
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc
##########
@@ -172,6 +190,64 @@ class TestUnaryArithmeticUnsigned : public
TestUnaryArithmeticIntegral<T> {};
template <typename T>
class TestUnaryArithmeticFloating : public TestUnaryArithmetic<T> {};
+TYPED_TEST_SUITE(TestUnaryArithmeticIntegral, IntegralTypes);
+TYPED_TEST_SUITE(TestUnaryArithmeticSigned, SignedIntegerTypes);
+TYPED_TEST_SUITE(TestUnaryArithmeticUnsigned, UnsignedIntegerTypes);
+TYPED_TEST_SUITE(TestUnaryArithmeticFloating, FloatingTypes);
+
+template <typename T>
+class TestUnaryRound : public TestBaseUnaryArithmetic<T, RoundOptions> {
+ protected:
+ using Base = TestBaseUnaryArithmetic<T, RoundOptions>;
+ using Base::options_;
+ void SetRoundMode(RoundMode value) { options_.round_mode = value; }
+ void SetRoundNdigits(int64_t value) { options_.ndigits = value; }
+};
+
+template <typename T>
+class TestUnaryRoundIntegral : public TestUnaryRound<T> {};
+
+template <typename T>
+class TestUnaryRoundSigned : public TestUnaryRoundIntegral<T> {};
+
+template <typename T>
+class TestUnaryRoundUnsigned : public TestUnaryRoundIntegral<T> {};
+
+template <typename T>
+class TestUnaryRoundFloating : public TestUnaryRound<T> {};
+
+TYPED_TEST_SUITE(TestUnaryRoundIntegral, IntegralTypes);
+TYPED_TEST_SUITE(TestUnaryRoundSigned, SignedIntegerTypes);
+TYPED_TEST_SUITE(TestUnaryRoundUnsigned, UnsignedIntegerTypes);
+TYPED_TEST_SUITE(TestUnaryRoundFloating, FloatingTypes);
Review comment:
As I said, can you move the `TYPED_TEST_SUITE` declarations near the
`TYPED_TEST` definitions?
##########
File path: cpp/src/arrow/compute/kernels/scalar_arithmetic.cc
##########
@@ -852,24 +853,243 @@ struct LogbChecked {
}
};
+struct RoundUtil {
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsInteger(
+ const T val) {
+ // |frac| ~ 0.0?
+ return std::floor(val) == val;
+ }
+
+ template <typename T>
+ static constexpr enable_if_t<std::is_floating_point<T>::value, bool>
IsHalfInteger(
+ const T val) {
+ // |frac| ~ 0.5?
+ return (val - std::floor(val)) == T(0.5);
+ }
+
+ // Calculate powers of ten with arbitrary integer exponent
+ template <typename T = double>
+ static enable_if_floating_point<T> Pow10(int64_t power) {
+ static constexpr T lut[] = {1e0F, 1e1F, 1e2F, 1e3F, 1e4F, 1e5F, 1e6F,
1e7F,
+ 1e8F, 1e9F, 1e10F, 1e11F, 1e12F, 1e13F, 1e14F,
1e15F};
+ int64_t lut_size = (sizeof(lut) / sizeof(*lut));
+ int64_t abs_power = std::abs(power);
+ auto pow10 = lut[std::min(abs_power, lut_size - 1)];
+ while (abs_power-- >= lut_size) {
+ pow10 *= 1e1F;
+ }
+ return (power >= 0) ? pow10 : (1 / pow10);
+ }
+};
+
+// Specializations of rounding implementations for round kernels
+template <typename, RoundMode>
+struct RoundImpl;
+
+template <typename T>
+struct RoundImpl<T, RoundMode::DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::trunc(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::signbit(val) ? std::floor(val) : std::ceil(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_DOWN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::DOWN>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_UP> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::UP>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_ZERO> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_ZERO>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TOWARDS_INFINITY> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return RoundImpl<T, RoundMode::TOWARDS_INFINITY>::Round(val);
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_EVEN> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::round(val * T(0.5)) * 2;
+ }
+};
+
+template <typename T>
+struct RoundImpl<T, RoundMode::HALF_TO_ODD> {
+ static constexpr enable_if_floating_point<T> Round(const T val) {
+ return std::floor(val * T(0.5)) + std::ceil(val * T(0.5));
+ }
+};
+
+// Specializations of kernel state for round kernels
+template <typename>
+struct RoundOptionsWrapper;
+
+template <>
+struct RoundOptionsWrapper<RoundOptions> : public OptionsWrapper<RoundOptions>
{
+ using OptionsType = RoundOptions;
+ double pow10;
+
+ explicit RoundOptionsWrapper(OptionsType options) :
OptionsWrapper(std::move(options)) {
+ // Only positive of powers of 10 are used because combining multiply and
+ // division operations produced more stable rounding than using
multiply-only.
+ // Refer to NumPy's round implementation:
+ //
https://github.com/numpy/numpy/blob/7b2f20b406d27364c812f7a81a9c901afbd3600c/numpy/core/src/multiarray/calculation.c#L589
+ pow10 = RoundUtil::Pow10(std::abs(options.ndigits));
+ }
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ if (auto options = static_cast<const OptionsType*>(args.options)) {
+ return
arrow::internal::make_unique<RoundOptionsWrapper<OptionsType>>(*options);
+ }
+
+ return Status::Invalid(
+ "Attempted to initialize KernelState from null FunctionOptions");
+ }
+};
+
+template <>
+struct RoundOptionsWrapper<RoundToMultipleOptions>
+ : public OptionsWrapper<RoundToMultipleOptions> {
+ using OptionsType = RoundToMultipleOptions;
+
+ static Result<std::unique_ptr<KernelState>> Init(KernelContext* ctx,
+ const KernelInitArgs& args)
{
+ ARROW_ASSIGN_OR_RAISE(auto state, OptionsWrapper<OptionsType>::Init(ctx,
args));
+ auto options = Get(*state);
+ if (options.multiple <= 0) {
+ return Status::Invalid("Rounding multiple has to be a non-zero positive
value");
+ }
+ return std::move(state);
+ }
+};
+
+template <RoundMode RndMode>
+struct Round {
+ using State = RoundOptionsWrapper<RoundOptions>;
+
+ template <typename T, typename Arg>
+ static enable_if_floating_point<Arg, T> Call(KernelContext* ctx, Arg arg,
Status* st) {
+ static_assert(std::is_same<T, Arg>::value, "");
+ // Do not process Inf or NaN because they will trigger the overflow error
at end of
+ // function.
+ if (!std::isfinite(arg)) {
+ return arg;
+ }
+ auto state = static_cast<State*>(ctx->state());
+ auto options = state->options;
+ auto pow10 = T(state->pow10);
+ auto round_val = (options.ndigits >= 0) ? (arg * pow10) : (arg / pow10);
+ // Use std::round() if in tie-breaking mode and scaled value is not 0.5.
+ if ((options.round_mode >= RoundMode::HALF_DOWN) &&
+ !RoundUtil::IsHalfInteger(round_val)) {
+ round_val = std::round(round_val);
+ } else if (!RoundUtil::IsInteger(round_val)) {
+ round_val = RoundImpl<T, RndMode>::Round(round_val);
+ }
+ // Equality check is ommitted so that the common case of 10^0 (integer
rounding) uses
+ // multiply-only
+ round_val = (options.ndigits > 0) ? (round_val / pow10) : (round_val *
pow10);
+ if (!std::isfinite(round_val)) {
+ *st = Status::Invalid("overflow occurred during rounding");
+ return arg;
+ }
+ return round_val;
+ }
+};
+
+template <RoundMode RndMode>
+struct RoundToMultiple {
+ using State = RoundOptionsWrapper<RoundToMultipleOptions>;
+
+ template <typename T, typename Arg>
+ static enable_if_floating_point<Arg, T> Call(KernelContext* ctx, Arg arg,
Status* st) {
+ static_assert(std::is_same<T, Arg>::value, "");
+ // Do not process Inf or NaN because they will trigger the overflow error
at end of
+ // function.
+ if (!std::isfinite(arg)) {
+ return arg;
+ }
+
+ auto options = State::Get(ctx);
+ auto round_val = arg / T(options.multiple);
+ // Use std::round() if in tie-breaking mode and scaled value is not 0.5.
+ if ((options.round_mode >= RoundMode::HALF_DOWN) &&
Review comment:
Same questions as above.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]