diff --git a/cpp/src/arrow/compute/api_scalar.cc b/cpp/src/arrow/compute/api_scalar.cc index 7c3bc46650e9f..f00fad29d7697 100644 --- a/cpp/src/arrow/compute/api_scalar.cc +++ b/cpp/src/arrow/compute/api_scalar.cc @@ -744,6 +744,7 @@ SCALAR_ARITHMETIC_UNARY(Sin, "sin", "sin_checked") SCALAR_ARITHMETIC_UNARY(Tan, "tan", "tan_checked") SCALAR_EAGER_UNARY(Atan, "atan") SCALAR_EAGER_UNARY(Exp, "exp") +SCALAR_EAGER_UNARY(Expm1, "expm1") SCALAR_EAGER_UNARY(Sign, "sign") Result Round(const Datum& arg, RoundOptions options, ExecContext* ctx) { diff --git a/cpp/src/arrow/compute/api_scalar.h b/cpp/src/arrow/compute/api_scalar.h index 947474e5962d0..21daf936fd236 100644 --- a/cpp/src/arrow/compute/api_scalar.h +++ b/cpp/src/arrow/compute/api_scalar.h @@ -684,6 +684,18 @@ Result Power(const Datum& left, const Datum& right, ARROW_EXPORT Result Exp(const Datum& arg, ExecContext* ctx = NULLPTR); +/// \brief More accurately calculate `exp(arg) - 1` for values close to zero. +/// If the exponent value is null the result will be null. +/// +/// This function is more accurate than calculating `exp(value) - 1` directly for values +/// close to zero. +/// +/// \param[in] arg the exponent +/// \param[in] ctx the function execution context, optional +/// \return the element-wise Euler's number raised to the power of exponent minus 1 +ARROW_EXPORT +Result Expm1(const Datum& arg, ExecContext* ctx = NULLPTR); + /// \brief Left shift the left array by the right array. Array values must be the /// same length. If either operand 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 d59320d270e4f..f045e323b3d0b 100644 --- a/cpp/src/arrow/compute/kernels/base_arithmetic_internal.h +++ b/cpp/src/arrow/compute/kernels/base_arithmetic_internal.h @@ -532,6 +532,14 @@ struct Exp { } }; +struct Expm1 { + template + static T Call(KernelContext*, Arg exp, Status*) { + static_assert(std::is_same::value); + return std::expm1(exp); + } +}; + struct Power { ARROW_NOINLINE static uint64_t IntegerPower(uint64_t base, uint64_t exp) { diff --git a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc index eb243de4a765e..f11449ad5741f 100644 --- a/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc +++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic.cc @@ -1087,6 +1087,12 @@ const FunctionDoc exp_doc{ ("If exponent is null the result will be null."), {"exponent"}}; +const FunctionDoc expm1_doc{ + "Compute Euler's number raised to the power of specified exponent, " + "then decrement 1, element-wise", + ("If exponent is null the result will be null."), + {"exponent"}}; + const FunctionDoc pow_checked_doc{ "Raise arguments to power element-wise", ("An error is returned when integer to negative integer power is encountered,\n" @@ -1614,6 +1620,10 @@ void RegisterScalarArithmetic(FunctionRegistry* registry) { auto exp = MakeUnaryArithmeticFunctionFloatingPoint("exp", exp_doc); DCHECK_OK(registry->AddFunction(std::move(exp))); + // ---------------------------------------------------------------------- + auto expm1 = MakeUnaryArithmeticFunctionFloatingPoint("expm1", expm1_doc); + DCHECK_OK(registry->AddFunction(std::move(expm1))); + // ---------------------------------------------------------------------- auto sqrt = MakeUnaryArithmeticFunctionFloatingPoint("sqrt", sqrt_doc); DCHECK_OK(registry->AddFunction(std::move(sqrt))); diff --git a/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc b/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc index 37a1bcbc02d73..9cabebc3f4e46 100644 --- a/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_arithmetic_test.cc @@ -43,6 +43,9 @@ namespace arrow { namespace compute { namespace { +// 2.718281828459045090795598298427648842334747314453125 +constexpr double kEuler64 = 0x1.5bf0a8b145769p+1; + using IntegralTypes = testing::Types; @@ -1485,8 +1488,7 @@ TYPED_TEST(TestUnaryArithmeticUnsigned, Exp) { this->AssertUnaryOp( exp, "[null, 1, 10]", ArrayFromJSON(float64(), "[null, 2.718281828459045, 22026.465794806718]")); - this->AssertUnaryOp(exp, this->MakeScalar(1), - arrow::MakeScalar(2.718281828459045F)); + this->AssertUnaryOp(exp, this->MakeScalar(1), arrow::MakeScalar(kEuler64)); } TYPED_TEST(TestUnaryArithmeticSigned, Exp) { @@ -1502,8 +1504,7 @@ TYPED_TEST(TestUnaryArithmeticSigned, Exp) { ArrayFromJSON(float64(), "[0.000045399929762484854, 0.36787944117144233, " "null, 2.718281828459045, 22026.465794806718]")); - this->AssertUnaryOp(exp, this->MakeScalar(1), - arrow::MakeScalar(2.718281828459045F)); + this->AssertUnaryOp(exp, this->MakeScalar(1), arrow::MakeScalar(kEuler64)); } TYPED_TEST(TestUnaryArithmeticFloating, Exp) { @@ -1563,6 +1564,101 @@ TEST_F(TestUnaryArithmeticDecimal, Exp) { } } +TYPED_TEST(TestUnaryArithmeticUnsigned, Expm1) { + auto expm1 = [](const Datum& arg, ArithmeticOptions, ExecContext* ctx) { + return Expm1(arg, ctx); + }; + // Empty arrays + this->AssertUnaryOp(expm1, "[]", ArrayFromJSON(float64(), "[]")); + // Array with nulls + this->AssertUnaryOp(expm1, "[null]", ArrayFromJSON(float64(), "[null]")); + this->AssertUnaryOp(expm1, this->MakeNullScalar(), arrow::MakeNullScalar(float64())); + this->AssertUnaryOp( + expm1, "[null, 0, 1, 10]", + ArrayFromJSON(float64(), "[null, 0.0, 1.718281828459045, 22025.465794806718]")); + this->AssertUnaryOp(expm1, this->MakeScalar(1), arrow::MakeScalar(kEuler64 - 1.0)); +} + +TYPED_TEST(TestUnaryArithmeticSigned, Expm1) { + auto expm1 = [](const Datum& arg, ArithmeticOptions, ExecContext* ctx) { + return Expm1(arg, ctx); + }; + // Empty arrays + this->AssertUnaryOp(expm1, "[]", ArrayFromJSON(float64(), "[]")); + // Array with nulls + this->AssertUnaryOp(expm1, "[null]", ArrayFromJSON(float64(), "[null]")); + this->AssertUnaryOp(expm1, this->MakeNullScalar(), arrow::MakeNullScalar(float64())); + this->AssertUnaryOp(expm1, "[-10, -1, 0, null, 1, 10]", + ArrayFromJSON(float64(), + "[-0.9999546000702375, -0.6321205588285577, 0.0, " + "null, 1.718281828459045, 22025.465794806718]")); + this->AssertUnaryOp(expm1, this->MakeScalar(1), arrow::MakeScalar(kEuler64 - 1.0)); +} + +TYPED_TEST(TestUnaryArithmeticFloating, Expm1) { + using CType = typename TestFixture::CType; + + auto min = std::numeric_limits::lowest(); + auto max = std::numeric_limits::max(); + + auto expm1 = [](const Datum& arg, ArithmeticOptions, ExecContext* ctx) { + return Expm1(arg, ctx); + }; + // Empty arrays + this->AssertUnaryOp(expm1, "[]", "[]"); + // Array with nulls + this->AssertUnaryOp(expm1, "[null]", "[null]"); + this->AssertUnaryOp(expm1, this->MakeNullScalar(), this->MakeNullScalar()); + this->AssertUnaryOp(expm1, "[-1.0, 0.0, 0.1, 0.00000001, null, 10.0]", + "[-0.6321205588285577, 0.0, " + "0.10517091807564763, 0.000000010000000050000001, " + "null, 22025.465794806718]"); + // Ordinary arrays (positive, negative, fractional, and zero inputs) + this->AssertUnaryOp(expm1, "[-10.0, 0.0, 0.1, 0.00000001, 0.5, 1.0]", + "[-0.9999546000702375, 0.0, " + "0.10517091807564763, 0.000000010000000050000001, " + "0.6487212707001282, 1.718281828459045]"); + this->AssertUnaryOp(expm1, 1.3F, 2.6692964926535487F); + this->AssertUnaryOp(expm1, this->MakeScalar(1.3F), + this->MakeScalar(2.6692964926535487F)); + // Arrays with infinites + this->AssertUnaryOp(expm1, "[-Inf, Inf]", "[-1, Inf]"); + // Arrays with NaNs + this->SetNansEqual(true); + this->AssertUnaryOp(expm1, "[NaN]", "[NaN]"); + // Min/max + this->AssertUnaryOp(expm1, min, -1.0); + this->AssertUnaryOp(expm1, max, std::numeric_limits::infinity()); +} + +TEST_F(TestUnaryArithmeticDecimal, Expm1) { + auto max128 = Decimal128::GetMaxValue(38); + auto max256 = Decimal256::GetMaxValue(76); + const auto func = "expm1"; + for (const auto& ty : PositiveScaleTypes()) { + CheckScalar(func, {ArrayFromJSON(ty, R"([])")}, ArrayFromJSON(float64(), "[]")); + CheckScalar( + func, {ArrayFromJSON(ty, R"(["-1.00", "0.00", "0.10", "0.01", "10.00", null])")}, + ArrayFromJSON(float64(), + "[-0.6321205588285577, 0.0, " + "0.10517091807564763, 0.010050167084168058, " + "22025.465794806718, null]")); + } + CheckScalar(func, {std::make_shared(max128, decimal128(38, 0))}, + ScalarFromJSON(float64(), "Inf")); + CheckScalar(func, {std::make_shared(-max128, decimal128(38, 0))}, + ScalarFromJSON(float64(), "-1.0")); + CheckScalar(func, {std::make_shared(max256, decimal256(76, 0))}, + ScalarFromJSON(float64(), "Inf")); + CheckScalar(func, {std::make_shared(-max256, decimal256(76, 0))}, + ScalarFromJSON(float64(), "-1.0")); + for (const auto& ty : NegativeScaleTypes()) { + CheckScalar(func, {ArrayFromJSON(ty, R"([])")}, ArrayFromJSON(float64(), "[]")); + CheckScalar(func, {DecimalArrayFromJSON(ty, R"(["12E2", "0", "-42E2", null])")}, + ArrayFromJSON(float64(), "[Inf, 0.0, -1.0, null]")); + } +} + TEST_F(TestUnaryArithmeticDecimal, Log) { std::vector unchecked = {"ln", "log2", "log10", "log1p"}; std::vector checked = {"ln_checked", "log2_checked", "log10_checked", diff --git a/docs/source/cpp/compute.rst b/docs/source/cpp/compute.rst index 093b160d8e9a0..3c264fb4767a1 100644 --- a/docs/source/cpp/compute.rst +++ b/docs/source/cpp/compute.rst @@ -476,6 +476,8 @@ Mixed time resolution temporal inputs will be cast to finest input resolution. +------------------+--------+-------------------------+---------------------------+-------+ | exp | Unary | Numeric | Float32/Float64 | | +------------------+--------+-------------------------+---------------------------+-------+ +| expm1 | Unary | Numeric | Float32/Float64 | | ++------------------+--------+-------------------------+---------------------------+-------+ | multiply | Binary | Numeric/Temporal | Numeric/Temporal | \(1) | +------------------+--------+-------------------------+---------------------------+-------+ | multiply_checked | Binary | Numeric/Temporal | Numeric/Temporal | \(1) |