This is an automated email from the ASF dual-hosted git repository.

pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 9b1fbede84e GH-50641: [C++][Compute] Fix correctness error in decimal 
round_binary kernel (#50642)
9b1fbede84e is described below

commit 9b1fbede84e54ef27ceda41869bc24ea03ad8b78
Author: Daniel Anderson <[email protected]>
AuthorDate: Tue Jul 28 01:36:16 2026 -0700

    GH-50641: [C++][Compute] Fix correctness error in decimal round_binary 
kernel (#50642)
    
    ### Rationale for this change
    
    There are major correctness bugs in the `round_binary` kernel for decimal 
input. #50641 describes them in more detail. Essentially, the midpoint 
`half_pow10` of the rounding range is computed outside of the loop and assumes 
that `ndigits` == 0. The consequence is that for round-to-nearest modes, 
`round_binary` will frequently give wrong answers when `ndigits` != 0. Also as 
a result of assuming `ndigits` is always 0, the function does not attempt to 
round negative scale inputs.
    
    There is also a correctness issue with the decimal implementation of the 
HALF_TO_ODD rounding mode, which applies both to the `round` and `round_binary` 
compute functions when the input is negative. In this case, the function does 
round to an odd, but rounds to the closest odd greater than the input rather 
than the nearest odd.
    
    ### What changes are included in this PR?
    
    The mentioned bugs are fixed. For `round_binary`, we make sure to compute 
`pow` and `half_pow10` inside `RoundBinary::Call` where we know the row value 
of `ndigits`.
    
    To fix the HALF_TO_ODD mode, we check whether the sign of the remainder is 
positive (1) instead of the truthy value of the sign (which turns out to be 
always `true` since `remainder.Sign()` is either 1 or -1. This matches the 
HALF_TO_EVEN implementation.
    
    Added decimal `round_binary` unit tests to 
`scalar_round_arithmetic_test.cc`. They were absent up to this point which is 
why the bug wasn't caught previously. I did some refactoring and added a couple 
structs to make it easier to share test data between 
TestUnaryRoundArithmeticDecimal and the new TestBinaryRoundArithmeticDecimal. 
Those test cases use a constant `ndigits` for every row, but serve to verify 
that `round_binary` works as well as `round` if used in the same way. I also 
cre [...]
    
    ### Are these changes tested?
    
    Yes. The new TestBinaryRoundArithmeticDecimal tests provide relatively 
extensive coverage of the `round_binary` kernel. I confirmed that they failed 
without the changes and passed with the fixes in place. There are a couple 
numbers included in the `TestBinaryRoundArithmeticDecimal.RoundNDigitsArray` 
test data that require the HALF_TO_ODD fix to be rounded correctly.
    
    Besides that, I ran the main Arrow C++ unit tests and Arrow compute tests 
to ensure they pass.
    
    ### Are there any user-facing changes?
    
    No user-facing changes.
    
    **This PR contains a "Critical Fix".** This PR fixes bugs in the decimal 
`round_binary` kernel and decimal HALF_TO_ODD rounding mode which cause 
incorrect data to be produced, even for the most common of inputs.
    
    * GitHub Issue: #50641
    
    Lead-authored-by: Daniel Anderson <[email protected]>
    Co-authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/compute/kernels/scalar_round.cc      |  24 +-
 .../kernels/scalar_round_arithmetic_test.cc        | 520 +++++++++++++--------
 2 files changed, 326 insertions(+), 218 deletions(-)

diff --git a/cpp/src/arrow/compute/kernels/scalar_round.cc 
b/cpp/src/arrow/compute/kernels/scalar_round.cc
index 208b9875a1c..f762f9e0fdd 100644
--- a/cpp/src/arrow/compute/kernels/scalar_round.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_round.cc
@@ -391,7 +391,7 @@ struct RoundImpl<Type, RoundMode::HALF_TO_ODD> {
                                                 const T& pow10, const int32_t 
scale) {
     auto scaled = val->ReduceScaleBy(scale, /*round=*/false);
     if (scaled.low_bits() % 2 == 0) {
-      scaled += remainder.Sign() ? 1 : -1;
+      scaled += remainder.Sign() >= 0 ? 1 : -1;
     }
     *val = scaled.IncreaseScaleBy(scale);
   }
@@ -915,29 +915,20 @@ struct RoundBinary<ArrowType, kRoundMode, 
enable_if_decimal<ArrowType>> {
   using CType = typename TypeTraits<ArrowType>::CType;
   using State = RoundOptionsWrapper<RoundBinaryOptions, double>;
   const ArrowType& ty;
-  int32_t pow;
-  // pow10 is "1" for the given decimal scale. Similarly half_pow10 is "0.5".
-  CType half_pow10, neg_half_pow10;
 
   explicit RoundBinary(const State& state, const DataType& out_ty)
       : RoundBinary(out_ty) {}
 
   explicit RoundBinary(const DataType& out_ty)
-      : ty(checked_cast<const ArrowType&>(out_ty)),
-        pow(static_cast<int32_t>(ty.scale() - 0)) {
-    if (pow >= ty.precision() || pow < 0) {
-      half_pow10 = neg_half_pow10 = 0;
-    } else {
-      half_pow10 = CType::GetHalfScaleMultiplier(pow);
-      neg_half_pow10 = -half_pow10;
-    }
-  }
+      : ty(checked_cast<const ArrowType&>(out_ty)) {}
 
   template <typename T = ArrowType, typename CType0 = typename 
TypeTraits<T>::CType0,
             typename CType1 = typename TypeTraits<T>::CType1>
   enable_if_decimal_value<CType> Call(KernelContext* ctx, CType0 arg0, CType1 
arg1,
                                       Status* st) const {
-    if (pow - arg1 >= ty.precision()) {
+    int32_t pow = static_cast<int32_t>(ty.scale() - arg1);
+
+    if (pow >= ty.precision()) {
       *st = Status::Invalid("Rounding to ", arg1, " digits will not fit in 
precision of ",
                             ty);
       return 0;
@@ -946,7 +937,8 @@ struct RoundBinary<ArrowType, kRoundMode, 
enable_if_decimal<ArrowType>> {
       return arg0;
     }
 
-    CType0 pow10 = CType0::GetScaleMultiplier(static_cast<int32_t>(ty.scale() 
- arg1));
+    // pow10 is "1" for the given decimal scale. Similarly half_pow10 is "0.5".
+    CType0 pow10 = CType0::GetScaleMultiplier(pow);
 
     std::pair<CType, CType> pair;
     *st = arg0.Divide(pow10).Value(&pair);
@@ -955,6 +947,8 @@ struct RoundBinary<ArrowType, kRoundMode, 
enable_if_decimal<ArrowType>> {
     const auto& remainder = pair.second;
     if (remainder == 0) return arg0;
     if (kRoundMode >= RoundMode::HALF_DOWN) {
+      CType0 half_pow10 = CType::GetHalfScaleMultiplier(pow);
+      CType0 neg_half_pow10 = -half_pow10;
       if (remainder == half_pow10 || remainder == neg_half_pow10) {
         // On the halfway point, use tiebreaker
         RoundImpl<CType0, kRoundMode>::Round(&arg0, remainder, pow10, pow);
diff --git a/cpp/src/arrow/compute/kernels/scalar_round_arithmetic_test.cc 
b/cpp/src/arrow/compute/kernels/scalar_round_arithmetic_test.cc
index a572af4195c..1e9e0c9d50c 100644
--- a/cpp/src/arrow/compute/kernels/scalar_round_arithmetic_test.cc
+++ b/cpp/src/arrow/compute/kernels/scalar_round_arithmetic_test.cc
@@ -507,218 +507,188 @@ TEST(TestUnaryRound, DispatchBestRound) {
   }
 }
 
-class TestUnaryRoundArithmeticDecimal : public TestRoundArithmeticDecimal {};
+enum class RoundTestCheckFunc {
+  kCheckScalar,
+  kCheckRaises,
+};
+
+// Const ndigits test case used by both TestUnaryRoundArithmeticDecimal and
+// TestBinaryRoundArithmeticDecimal
+struct RoundDecimalTestCase {
+  RoundTestCheckFunc check_func;
+  int32_t ndigits;
+  RoundMode round_mode;
+  std::string expected;  // Expected result array or expected error msg
+};
+
+struct RoundDecimalTestVariant {
+  std::vector<std::shared_ptr<DataType>> decimal_types;
+  std::string input_json;
+  std::vector<RoundDecimalTestCase> cases;
+  bool scientific_data = false;  // Whether to use DecimalArrayFromJSON
+};
+
+std::vector<RoundDecimalTestVariant> GetRoundDecimalTestCases() {
+  using enum RoundTestCheckFunc;
+  return {
+      {{decimal128(4, 3), decimal256(4, 3)},
+       R"(["1.010", "1.012", "1.015", "1.019", "-1.010", "-1.012", "-1.015", 
"-1.019", null])",
+       {{kCheckScalar, 2, RoundMode::DOWN,
+         R"(["1.010", "1.010", "1.010", "1.010", "-1.010", "-1.020", "-1.020", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::UP,
+         R"(["1.010", "1.020", "1.020", "1.020", "-1.010", "-1.010", "-1.010", 
"-1.010", null])"},
+        {kCheckScalar, 2, RoundMode::TOWARDS_ZERO,
+         R"(["1.010", "1.010", "1.010", "1.010", "-1.010", "-1.010", "-1.010", 
"-1.010", null])"},
+        {kCheckScalar, 2, RoundMode::TOWARDS_INFINITY,
+         R"(["1.010", "1.020", "1.020", "1.020", "-1.010", "-1.020", "-1.020", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_DOWN,
+         R"(["1.010", "1.010", "1.010", "1.020", "-1.010", "-1.010", "-1.020", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_UP,
+         R"(["1.010", "1.010", "1.020", "1.020", "-1.010", "-1.010", "-1.010", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_TOWARDS_ZERO,
+         R"(["1.010", "1.010", "1.010", "1.020", "-1.010", "-1.010", "-1.010", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_TOWARDS_INFINITY,
+         R"(["1.010", "1.010", "1.020", "1.020", "-1.010", "-1.010", "-1.020", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_TO_EVEN,
+         R"(["1.010", "1.010", "1.020", "1.020", "-1.010", "-1.010", "-1.020", 
"-1.020", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_TO_ODD,
+         R"(["1.010", "1.010", "1.010", "1.020", "-1.010", "-1.010", "-1.010", 
"-1.020", null])"}}}};
+}
+
+std::vector<RoundDecimalTestVariant> GetRoundTowardsInfinityDecimalTestCases() 
{
+  using enum RoundTestCheckFunc;
+  return {{{decimal128(4, 2), decimal256(4, 2)},
+           R"(["1.00", "1.99", "1.01", "-42.00", "-42.99", "-42.15", null])",
+           {
+               {kCheckScalar, 0, RoundMode::TOWARDS_INFINITY,
+                R"(["1.00", "2.00", "2.00", "-42.00", "-43.00", "-43.00", 
null])"},
+               {kCheckScalar, 1, RoundMode::TOWARDS_INFINITY,
+                R"(["1.00", "2.00", "1.10", "-42.00", "-43.00", "-42.20", 
null])"},
+               {kCheckScalar, 2, RoundMode::TOWARDS_INFINITY,
+                R"(["1.00", "1.99", "1.01", "-42.00", "-42.99", "-42.15", 
null])"},
+               {kCheckScalar, 4, RoundMode::TOWARDS_INFINITY,
+                R"(["1.00", "1.99", "1.01", "-42.00", "-42.99", "-42.15", 
null])"},
+               {kCheckScalar, 100, RoundMode::TOWARDS_INFINITY,
+                R"(["1.00", "1.99", "1.01", "-42.00", "-42.99", "-42.15", 
null])"},
+               {kCheckScalar, -1, RoundMode::TOWARDS_INFINITY,
+                R"(["10.00", "10.00", "10.00", "-50.00", "-50.00", "-50.00", 
null])"},
+               {kCheckRaises, -2, RoundMode::TOWARDS_INFINITY,
+                "Rounding to -2 digits will not fit in precision"},
+           }},
+          {{decimal128(4, 2), decimal256(4, 2)},
+           R"(["99.99"])",
+           {{kCheckRaises, -1, RoundMode::TOWARDS_INFINITY,
+             "Rounded value 100.00 does not fit in precision"}}},
+          {{decimal128(2, -2), decimal256(2, -2)},
+           R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])",
+           {{kCheckScalar, 0, RoundMode::TOWARDS_INFINITY,
+             R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])"},
+            {kCheckScalar, 2, RoundMode::TOWARDS_INFINITY,
+             R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])"},
+            {kCheckScalar, 100, RoundMode::TOWARDS_INFINITY,
+             R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])"},
+            {kCheckScalar, -1, RoundMode::TOWARDS_INFINITY,
+             R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])"},
+            {kCheckScalar, -2, RoundMode::TOWARDS_INFINITY,
+             R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])"},
+            {kCheckScalar, -3, RoundMode::TOWARDS_INFINITY,
+             R"(["10E2", "20E2", "20E2", "-10E2", "-20E2", "-20E2", null])"},
+            {kCheckRaises, -4, RoundMode::TOWARDS_INFINITY,
+             "Rounding to -4 digits will not fit in precision"}},
+           true}};
+}
+
+std::vector<RoundDecimalTestVariant> GetRoundHalfToEvenDecimalTestCases() {
+  using enum RoundTestCheckFunc;
+  return {
+      {{decimal128(4, 2), decimal256(4, 2)},
+       R"(["1.00", "5.99", "1.01", "-42.00", "-42.99", "-42.15", "1.50", 
"2.50", "-5.50", "-2.55", null])",
+       {
+           {kCheckScalar, 0, RoundMode::HALF_TO_EVEN,
+            R"(["1.00", "6.00", "1.00", "-42.00", "-43.00", "-42.00", "2.00", 
"2.00", "-6.00", "-3.00", null])"},
+           {kCheckScalar, 1, RoundMode::HALF_TO_EVEN,
+            R"(["1.00", "6.00", "1.00", "-42.00", "-43.00", "-42.20", "1.50", 
"2.50", "-5.50", "-2.60", null])"},
+           {kCheckScalar, 2, RoundMode::HALF_TO_EVEN,
+            R"(["1.00", "5.99", "1.01", "-42.00", "-42.99", "-42.15", "1.50", 
"2.50", "-5.50", "-2.55", null])"},
+           {kCheckScalar, 4, RoundMode::HALF_TO_EVEN,
+            R"(["1.00", "5.99", "1.01", "-42.00", "-42.99", "-42.15", "1.50", 
"2.50", "-5.50", "-2.55", null])"},
+           {kCheckScalar, 100, RoundMode::HALF_TO_EVEN,
+            R"(["1.00", "5.99", "1.01", "-42.00", "-42.99", "-42.15", "1.50", 
"2.50", "-5.50", "-2.55", null])"},
+           {kCheckScalar, -1, RoundMode::HALF_TO_EVEN,
+            R"(["0.00", "10.00", "0.00", "-40.00", "-40.00", "-40.00", "0.00", 
"0.00", "-10.00", "0.00", null])"},
+           {kCheckRaises, -2, RoundMode::HALF_TO_EVEN,
+            "Rounding to -2 digits will not fit in precision"},
+       }},
+      {{decimal128(4, 2), decimal256(4, 2)},
+       R"(["99.99"])",
+       {{kCheckRaises, -1, RoundMode::HALF_TO_EVEN,
+         "Rounded value 100.00 does not fit in precision"}}},
+      {{decimal128(2, -2), decimal256(2, -2)},
+       R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])",
+       {{kCheckScalar, 0, RoundMode::HALF_TO_EVEN,
+         R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])"},
+        {kCheckScalar, 2, RoundMode::HALF_TO_EVEN,
+         R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])"},
+        {kCheckScalar, 100, RoundMode::HALF_TO_EVEN,
+         R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])"},
+        {kCheckScalar, -1, RoundMode::HALF_TO_EVEN,
+         R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])"},
+        {kCheckScalar, -2, RoundMode::HALF_TO_EVEN,
+         R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])"},
+        {kCheckScalar, -3, RoundMode::HALF_TO_EVEN,
+         R"(["0", "10E2", "10E2", "20E2", "20E2", "-10E2", "-10E2", "-20E2", 
"-20E2", null])"},
+        {kCheckRaises, -4, RoundMode::HALF_TO_EVEN,
+         "Rounding to -4 digits will not fit in precision"}},
+       true}};
+}
+
+class TestUnaryRoundArithmeticDecimal : public TestRoundArithmeticDecimal {
+ protected:
+  void RunUnaryRoundDecimalTestVariants(
+      const std::string& round_func,
+      const std::vector<RoundDecimalTestVariant>& decimal_test_variants) {
+    RoundOptions options;
+    for (const auto& variant : decimal_test_variants) {
+      for (std::shared_ptr<DataType> ty : variant.decimal_types) {
+        std::shared_ptr<Array> values;
+        if (variant.scientific_data) {
+          values = DecimalArrayFromJSON(ty, variant.input_json);
+        } else {
+          values = ArrayFromJSON(ty, variant.input_json);
+        }
+
+        for (const auto& test_case : variant.cases) {
+          options.ndigits = test_case.ndigits;
+          options.round_mode = test_case.round_mode;
+
+          if (test_case.check_func == RoundTestCheckFunc::kCheckScalar) {
+            std::shared_ptr<Array> expected_arr;
+            if (variant.scientific_data) {
+              expected_arr = DecimalArrayFromJSON(ty, test_case.expected);
+            } else {
+              expected_arr = ArrayFromJSON(ty, test_case.expected);
+            }
+            CheckScalar(round_func, {values}, expected_arr, &options);
+          } else {  // test_case.check_func == RoundTestCheckFunc.kCheckRaises
+            CheckRaises(round_func, {values}, test_case.expected, &options);
+          }
+        }
+      }
+    }
+  }
+};
 
 // Check two modes exhaustively, give all modes a simple test
 TEST_F(TestUnaryRoundArithmeticDecimal, Round) {
-  const auto func = "round";
-  RoundOptions options(2, RoundMode::DOWN);
-  for (const auto& ty : {decimal128(4, 3), decimal256(4, 3)}) {
-    auto values = ArrayFromJSON(
-        ty,
-        R"(["1.010", "1.012", "1.015", "1.019", "-1.010", "-1.012", "-1.015", 
"-1.019", null])");
-    options.round_mode = RoundMode::DOWN;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.010", "1.010", "-1.010", "-1.020", 
"-1.020", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::UP;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.020", "1.020", "1.020", "-1.010", "-1.010", 
"-1.010", "-1.010", null])"),
-        &options);
-    options.round_mode = RoundMode::TOWARDS_ZERO;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.010", "1.010", "-1.010", "-1.010", 
"-1.010", "-1.010", null])"),
-        &options);
-    options.round_mode = RoundMode::TOWARDS_INFINITY;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.020", "1.020", "1.020", "-1.010", "-1.020", 
"-1.020", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::HALF_DOWN;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.010", "1.020", "-1.010", "-1.010", 
"-1.020", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::HALF_UP;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.020", "1.020", "-1.010", "-1.010", 
"-1.010", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::HALF_TOWARDS_ZERO;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.010", "1.020", "-1.010", "-1.010", 
"-1.010", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::HALF_TOWARDS_INFINITY;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.020", "1.020", "-1.010", "-1.010", 
"-1.020", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::HALF_TO_EVEN;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.020", "1.020", "-1.010", "-1.010", 
"-1.020", "-1.020", null])"),
-        &options);
-    options.round_mode = RoundMode::HALF_TO_ODD;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.010", "1.010", "1.010", "1.020", "-1.010", "-1.010", 
"-1.010", "-1.020", null])"),
-        &options);
-  }
+  RunUnaryRoundDecimalTestVariants("round", GetRoundDecimalTestCases());
 }
 
 TEST_F(TestUnaryRoundArithmeticDecimal, RoundTowardsInfinity) {
-  const auto func = "round";
-  RoundOptions options(0, RoundMode::TOWARDS_INFINITY);
-  for (const auto& ty : {decimal128(4, 2), decimal256(4, 2)}) {
-    auto values = ArrayFromJSON(
-        ty, R"(["1.00", "1.99", "1.01", "-42.00", "-42.99", "-42.15", null])");
-    CheckScalar(func, {ArrayFromJSON(ty, R"([])")}, ArrayFromJSON(ty, 
R"([])"), &options);
-    options.ndigits = 0;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(ty,
-                      R"(["1.00", "2.00", "2.00", "-42.00", "-43.00", 
"-43.00", null])"),
-        &options);
-    options.ndigits = 1;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(ty,
-                      R"(["1.00", "2.00", "1.10", "-42.00", "-43.00", 
"-42.20", null])"),
-        &options);
-    options.ndigits = 2;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 4;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 100;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -1;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty, R"(["10.00", "10.00", "10.00", "-50.00", "-50.00", "-50.00", 
null])"),
-        &options);
-    options.ndigits = -2;
-    CheckRaises(func, {values}, "Rounding to -2 digits will not fit in 
precision",
-                &options);
-    options.ndigits = -1;
-    CheckRaises(func, {ArrayFromJSON(ty, R"(["99.99"])")},
-                "Rounded value 100.00 does not fit in precision", &options);
-  }
-  for (const auto& ty : {decimal128(2, -2), decimal256(2, -2)}) {
-    auto values = DecimalArrayFromJSON(
-        ty, R"(["10E2", "12E2", "18E2", "-10E2", "-12E2", "-18E2", null])");
-    options.ndigits = 0;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 2;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 100;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -1;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -2;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -3;
-    CheckScalar(func, {values},
-                DecimalArrayFromJSON(
-                    ty, R"(["10E2", "20E2", "20E2", "-10E2", "-20E2", "-20E2", 
null])"),
-                &options);
-    options.ndigits = -4;
-    CheckRaises(func, {values}, "Rounding to -4 digits will not fit in 
precision",
-                &options);
-  }
+  RunUnaryRoundDecimalTestVariants("round", 
GetRoundTowardsInfinityDecimalTestCases());
 }
 
 TEST_F(TestUnaryRoundArithmeticDecimal, RoundHalfToEven) {
-  const auto func = "round";
-  RoundOptions options(0, RoundMode::HALF_TO_EVEN);
-  for (const auto& ty : {decimal128(4, 2), decimal256(4, 2)}) {
-    auto values = ArrayFromJSON(
-        ty,
-        R"(["1.00", "5.99", "1.01", "-42.00", "-42.99", "-42.15", "1.50", 
"2.50", "-5.50", "-2.55", null])");
-    CheckScalar(func, {ArrayFromJSON(ty, R"([])")}, ArrayFromJSON(ty, 
R"([])"), &options);
-    options.ndigits = 0;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.00", "6.00", "1.00", "-42.00", "-43.00", "-42.00", "2.00", 
"2.00", "-6.00", "-3.00", null])"),
-        &options);
-    options.ndigits = 1;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["1.00", "6.00", "1.00", "-42.00", "-43.00", "-42.20", "1.50", 
"2.50", "-5.50", "-2.60", null])"),
-        &options);
-    options.ndigits = 2;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 4;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 100;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -1;
-    CheckScalar(
-        func, {values},
-        ArrayFromJSON(
-            ty,
-            R"(["0.00", "10.00", "0.00", "-40.00", "-40.00", "-40.00", "0.00", 
"0.00", "-10.00", "0.00", null])"),
-        &options);
-    options.ndigits = -2;
-    CheckRaises(func, {values}, "Rounding to -2 digits will not fit in 
precision",
-                &options);
-    options.ndigits = -1;
-    CheckRaises(func, {ArrayFromJSON(ty, R"(["99.99"])")},
-                "Rounded value 100.00 does not fit in precision", &options);
-  }
-  for (const auto& ty : {decimal128(2, -2), decimal256(2, -2)}) {
-    auto values = DecimalArrayFromJSON(
-        ty,
-        R"(["5E2", "10E2", "12E2", "15E2", "18E2", "-10E2", "-12E2", "-15E2", 
"-18E2", null])");
-    options.ndigits = 0;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 2;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = 100;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -1;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -2;
-    CheckScalar(func, {values}, values, &options);
-    options.ndigits = -3;
-    CheckScalar(
-        func, {values},
-        DecimalArrayFromJSON(
-            ty,
-            R"(["0", "10E2", "10E2", "20E2", "20E2", "-10E2", "-10E2", 
"-20E2", "-20E2", null])"),
-        &options);
-    options.ndigits = -4;
-    CheckRaises(func, {values}, "Rounding to -4 digits will not fit in 
precision",
-                &options);
-  }
+  RunUnaryRoundDecimalTestVariants("round", 
GetRoundHalfToEvenDecimalTestCases());
 }
 
 TEST_F(TestUnaryRoundArithmeticDecimal, RoundCeil) {
@@ -1007,6 +977,150 @@ TEST_F(TestUnaryRoundArithmeticDecimal, 
RoundToMultipleHalfToOdd) {
   }
 }
 
+class TestBinaryRoundArithmeticDecimal : public TestRoundArithmeticDecimal {
+ protected:
+  void RunBinaryRoundDecimalTestVariants(
+      const std::string& round_func,
+      const std::vector<RoundDecimalTestVariant>& decimal_test_variants) {
+    RoundBinaryOptions options;
+    for (const auto& variant : decimal_test_variants) {
+      for (std::shared_ptr<DataType> ty : variant.decimal_types) {
+        std::shared_ptr<Array> values;
+        if (variant.scientific_data) {
+          values = DecimalArrayFromJSON(ty, variant.input_json);
+        } else {
+          values = ArrayFromJSON(ty, variant.input_json);
+        }
+
+        for (const auto& test_case : variant.cases) {
+          options.round_mode = test_case.round_mode;
+
+          ASSERT_OK_AND_ASSIGN(auto ndigits_scalar,
+                               arrow::MakeScalar(int32(), test_case.ndigits));
+          ASSERT_OK_AND_ASSIGN(auto ndigits_arr,
+                               MakeArrayFromScalar(*ndigits_scalar, 
values->length()));
+
+          if (test_case.check_func == RoundTestCheckFunc::kCheckScalar) {
+            std::shared_ptr<Array> expected_arr;
+            if (variant.scientific_data) {
+              expected_arr = DecimalArrayFromJSON(ty, test_case.expected);
+            } else {
+              expected_arr = ArrayFromJSON(ty, test_case.expected);
+            }
+            CheckScalar(round_func, {values, ndigits_arr}, expected_arr, 
&options);
+          } else {  // test_case.check_func == RoundTestCheckFunc.kCheckRaises
+            CheckRaises(round_func, {values, ndigits_arr}, test_case.expected, 
&options);
+          }
+        }
+      }
+    }
+  }
+};
+
+// Check two modes exhaustively, give all modes a simple test
+TEST_F(TestBinaryRoundArithmeticDecimal, Round) {
+  RunBinaryRoundDecimalTestVariants("round_binary", 
GetRoundDecimalTestCases());
+}
+
+TEST_F(TestBinaryRoundArithmeticDecimal, RoundTowardsInfinity) {
+  RunBinaryRoundDecimalTestVariants("round_binary",
+                                    GetRoundTowardsInfinityDecimalTestCases());
+}
+
+TEST_F(TestBinaryRoundArithmeticDecimal, RoundHalfToEven) {
+  RunBinaryRoundDecimalTestVariants("round_binary", 
GetRoundHalfToEvenDecimalTestCases());
+}
+
+// Test binary round where ndigits varies per element
+TEST_F(TestBinaryRoundArithmeticDecimal, RoundNDigitsArray) {
+  const auto func = "round_binary";
+  RoundBinaryOptions options;
+
+  for (const auto& ty : {decimal128(4, 2), decimal256(4, 2)}) {
+    auto values =
+        ArrayFromJSON(ty,
+                      R"(["61.55", "61.55", "61.52", "60.50", "65.00", 
"65.95", "-61.55",
+                          "-61.55", "-61.52", "-60.50", "-65.00", "-65.95", 
null, "32.78"])");
+    auto ndigits_arr =
+        ArrayFromJSON(int32(), "[2, 1, 1, 0, -1, -1, 2, 1, 1, 0, -1, -1, 5, 
null]");
+
+    std::vector<std::pair<RoundMode, std::string>> round_modes_and_expected{{
+        {RoundMode::DOWN,
+         R"(["61.55", "61.50", "61.50", "60.00", "60.00", "60.00", "-61.55",
+             "-61.60", "-61.60", "-61.00", "-70.00", "-70.00", null, null])"},
+        {RoundMode::UP,
+         R"(["61.55", "61.60", "61.60", "61.00", "70.00", "70.00", "-61.55",
+             "-61.50", "-61.50", "-60.00", "-60.00", "-60.00", null, null])"},
+        {RoundMode::TOWARDS_ZERO,
+         R"(["61.55", "61.50", "61.50", "60.00", "60.00", "60.00", "-61.55",
+             "-61.50", "-61.50", "-60.00", "-60.00", "-60.00", null, null])"},
+        {RoundMode::TOWARDS_INFINITY,
+         R"(["61.55", "61.60", "61.60", "61.00", "70.00", "70.00", "-61.55",
+             "-61.60", "-61.60", "-61.00", "-70.00", "-70.00", null, null])"},
+        {RoundMode::HALF_DOWN,
+         R"(["61.55", "61.50", "61.50", "60.00", "60.00", "70.00", "-61.55",
+             "-61.60", "-61.50", "-61.00", "-70.00", "-70.00", null, null])"},
+        {RoundMode::HALF_UP,
+         R"(["61.55", "61.60", "61.50", "61.00", "70.00", "70.00", "-61.55",
+             "-61.50", "-61.50", "-60.00", "-60.00", "-70.00", null, null])"},
+        {RoundMode::HALF_TOWARDS_ZERO,
+         R"(["61.55", "61.50", "61.50", "60.00", "60.00", "70.00", "-61.55",
+             "-61.50", "-61.50", "-60.00", "-60.00", "-70.00", null, null])"},
+        {RoundMode::HALF_TOWARDS_INFINITY,
+         R"(["61.55", "61.60", "61.50", "61.00", "70.00", "70.00", "-61.55",
+             "-61.60", "-61.50", "-61.00", "-70.00", "-70.00", null, null])"},
+        {RoundMode::HALF_TO_EVEN,
+         R"(["61.55", "61.60", "61.50", "60.00", "60.00", "70.00", "-61.55",
+             "-61.60", "-61.50", "-60.00", "-60.00", "-70.00", null, null])"},
+        {RoundMode::HALF_TO_ODD,
+         R"(["61.55", "61.50", "61.50", "61.00", "70.00", "70.00", "-61.55",
+             "-61.50", "-61.50", "-61.00", "-70.00", "-70.00", null, null])"},
+    }};
+
+    for (const auto& pair : round_modes_and_expected) {
+      options.round_mode = pair.first;
+      CheckScalar(func, {values, ndigits_arr}, ArrayFromJSON(ty, pair.second), 
&options);
+    }
+  }
+
+  for (const auto& ty : {decimal128(2, -2), decimal256(2, -2)}) {
+    auto values = DecimalArrayFromJSON(
+        ty,
+        R"(["15E2", "15E2", "5E2", "15E2", "18E2", "-15E2", "-15E2", "-5E2", 
"-15E2", "-18E2", null, "15E2"])");
+    auto ndigits_arr =
+        ArrayFromJSON(int32(), "[100, -2, -3, -3, -3, 100, -2, -3, -3, -3, 5, 
null]");
+
+    std::vector<std::pair<RoundMode, std::string>> round_modes_and_expected{{
+        {RoundMode::DOWN,
+         R"(["15E2", "15E2", "0", "10E2", "10E2", "-15E2", "-15E2", "-10E2", 
"-20E2", "-20E2", null, null])"},
+        {RoundMode::UP,
+         R"(["15E2", "15E2", "10E2", "20E2", "20E2", "-15E2", "-15E2", "0", 
"-10E2", "-10E2", null, null])"},
+        {RoundMode::TOWARDS_ZERO,
+         R"(["15E2", "15E2", "0", "10E2", "10E2", "-15E2", "-15E2", "0", 
"-10E2", "-10E2", null, null])"},
+        {RoundMode::TOWARDS_INFINITY,
+         R"(["15E2", "15E2", "10E2", "20E2", "20E2", "-15E2", "-15E2", 
"-10E2", "-20E2", "-20E2", null, null])"},
+        {RoundMode::HALF_DOWN,
+         R"(["15E2", "15E2", "0", "10E2", "20E2", "-15E2", "-15E2", "-10E2", 
"-20E2", "-20E2", null, null])"},
+        {RoundMode::HALF_UP,
+         R"(["15E2", "15E2", "10E2", "20E2", "20E2", "-15E2", "-15E2", "0", 
"-10E2", "-20E2", null, null])"},
+        {RoundMode::HALF_TOWARDS_ZERO,
+         R"(["15E2", "15E2", "0", "10E2", "20E2", "-15E2", "-15E2", "0", 
"-10E2", "-20E2", null, null])"},
+        {RoundMode::HALF_TOWARDS_INFINITY,
+         R"(["15E2", "15E2", "10E2", "20E2", "20E2", "-15E2", "-15E2", 
"-10E2", "-20E2", "-20E2", null, null])"},
+        {RoundMode::HALF_TO_EVEN,
+         R"(["15E2", "15E2", "0", "20E2", "20E2", "-15E2", "-15E2", "0", 
"-20E2", "-20E2", null, null])"},
+        {RoundMode::HALF_TO_ODD,
+         R"(["15E2", "15E2", "10E2", "10E2", "20E2", "-15E2", "-15E2", 
"-10E2", "-10E2", "-20E2", null, null])"},
+    }};
+
+    for (const auto& pair : round_modes_and_expected) {
+      options.round_mode = pair.first;
+      CheckScalar(func, {values, ndigits_arr}, DecimalArrayFromJSON(ty, 
pair.second),
+                  &options);
+    }
+  }
+}
+
 TYPED_TEST_SUITE(TestUnaryRoundIntegral, IntegralTypes);
 TYPED_TEST_SUITE(TestUnaryRoundSigned, SignedIntegerTypes);
 TYPED_TEST_SUITE(TestUnaryRoundUnsigned, UnsignedIntegerTypes);

Reply via email to