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

kou 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 4bf9a16f7a GH-48408: [C++] Enable ULP-based float comparison (#49290)
4bf9a16f7a is described below

commit 4bf9a16f7a2cee7700698aa135287000dbf41a4a
Author: Arash Andishgar <[email protected]>
AuthorDate: Wed Jun 17 00:59:03 2026 +0330

    GH-48408: [C++] Enable ULP-based float comparison (#49290)
    
    ### Rationale for this change
    
    Enable ULP-based floating-point comparison.
    
    ### What changes are included in this PR?
    
    Add `arrow::EqualOptions::use_ulp_distance` and 
`arrow::EqualOptions::ulp_distance`.
    
    ### Are these changes tested?
    
    Yes.
    
    ### Are there any user-facing changes?
    
    Yes. The ULP-based comparison method is enabled via 
`arrow::EqualOptions::use_ulp_distance` and `arrow::EqualOptions::ulp_distance`.
    
    * GitHub Issue: #48408
    
    Lead-authored-by: arash andishgar <[email protected]>
    Co-authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Sutou Kouhei <[email protected]>
---
 c_glib/arrow-glib/basic-array.cpp                  |   5 +-
 cpp/src/arrow/CMakeLists.txt                       |   1 +
 cpp/src/arrow/array/array_test.cc                  |  52 ++++-
 cpp/src/arrow/array/statistics_test.cc             |   6 +-
 cpp/src/arrow/chunked_array.cc                     |   6 +-
 cpp/src/arrow/chunked_array.h                      |   4 +
 cpp/src/arrow/chunked_array_test.cc                |   4 +
 cpp/src/arrow/compare.cc                           | 258 +++++++++++----------
 cpp/src/arrow/compare.h                            |  49 +++-
 cpp/src/arrow/meson.build                          |   1 +
 cpp/src/arrow/record_batch.h                       |   9 +-
 cpp/src/arrow/record_batch_test.cc                 |   6 +-
 cpp/src/arrow/scalar_test.cc                       |  89 ++++++-
 cpp/src/arrow/table_test.cc                        |  14 +-
 cpp/src/arrow/testing/gtest_util_test.cc           | 184 +--------------
 cpp/src/arrow/testing/math.cc                      |  84 +------
 cpp/src/arrow/testing/math.h                       |  13 +-
 cpp/src/arrow/util/CMakeLists.txt                  |   1 +
 cpp/src/arrow/util/meson.build                     |   1 +
 .../{testing/math.cc => util/ulp_distance.cc}      |  62 +----
 .../math.h => util/ulp_distance_internal.h}        |  25 +-
 .../ulp_distance_test.cc}                          | 179 +-------------
 22 files changed, 400 insertions(+), 653 deletions(-)

diff --git a/c_glib/arrow-glib/basic-array.cpp 
b/c_glib/arrow-glib/basic-array.cpp
index bf5bf60d00..45ff4c1cae 100644
--- a/c_glib/arrow-glib/basic-array.cpp
+++ b/c_glib/arrow-glib/basic-array.cpp
@@ -279,7 +279,8 @@ garrow_equal_options_get_property(GObject *object,
     g_value_set_boolean(value, priv->options.nans_equal());
     break;
   case PROP_ABSOLUTE_TOLERANCE:
-    g_value_set_double(value, priv->options.atol());
+    g_value_set_double(value,
+                       priv->options.atol().has_value() ? 
*priv->options.atol() : 0.0);
     break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
@@ -348,7 +349,7 @@ garrow_equal_options_class_init(GArrowEqualOptionsClass 
*klass)
                              "of floating-point values",
                              -G_MAXDOUBLE,
                              G_MAXDOUBLE,
-                             options.atol(),
+                             options.atol().has_value() ? *options.atol() : 
0.0,
                              static_cast<GParamFlags>(G_PARAM_READWRITE));
   g_object_class_install_property(gobject_class, PROP_ABSOLUTE_TOLERANCE, 
spec);
 }
diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt
index 453a06f9a8..8750598f6c 100644
--- a/cpp/src/arrow/CMakeLists.txt
+++ b/cpp/src/arrow/CMakeLists.txt
@@ -576,6 +576,7 @@ set(ARROW_UTIL_SRCS
     util/time.cc
     util/tracing.cc
     util/trie.cc
+    util/ulp_distance.cc
     util/union_util.cc
     util/unreachable.cc
     util/uri.cc
diff --git a/cpp/src/arrow/array/array_test.cc 
b/cpp/src/arrow/array/array_test.cc
index 64ea3fd71a..dcfe1c76c3 100644
--- a/cpp/src/arrow/array/array_test.cc
+++ b/cpp/src/arrow/array/array_test.cc
@@ -2199,11 +2199,19 @@ void CheckFloatApproxEqualsWithAtol() {
   auto options = EqualOptions::Defaults().atol(0.2);
 
   ASSERT_FALSE(a->Equals(b));
+  ASSERT_TRUE(a->Equals(b, options));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(a->Equals(b, options.use_atol(true)));
+  ASSERT_FALSE(a->Equals(b, options.use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(a->ApproxEquals(b, options));
 
-  ASSERT_FALSE(a->RangeEquals(0, 1, 0, b, options));
+  ASSERT_FALSE(a->RangeEquals(0, 1, 0, b));
+  ASSERT_TRUE(a->RangeEquals(0, 1, 0, b, options));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(a->RangeEquals(0, 1, 0, b, options.use_atol(true)));
+  ASSERT_FALSE(a->RangeEquals(0, 1, 0, b, options.use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(ArrayRangeApproxEquals(*a, *b, 0, 1, 0, options));
 }
 
@@ -2413,6 +2421,42 @@ void CheckFloatingZeroEquality() {
   }
 }
 
+template <typename TYPE>
+void CheckFloatApproxEqualsWithUlpDistance() {
+  using CType =
+      std::conditional_t<is_half_float_type<TYPE>::value, Float16, typename 
TYPE::c_type>;
+  auto type = TypeTraits<TYPE>::type_singleton();
+  std::vector<CType> a, b;
+  a = {CType(NAN), CType(+0.0), CType(INFINITY)};
+  b = {CType(NAN), CType(-0.0), CType(INFINITY)};
+  if constexpr (is_half_float_type<TYPE>::value) {
+    a.push_back(Float16(1.00097656));
+    b.push_back(Float16(0.999511719f));
+  } else if constexpr (std::is_same_v<TYPE, DoubleType>) {
+    a.push_back(CType(0.9999999999999999));
+    b.push_back(CType(1.0000000000000002));
+  } else if constexpr (std::is_same_v<TYPE, FloatType>) {
+    a.push_back(CType(1.0000001f));
+    b.push_back(CType(0.99999994f));
+  }
+
+  std::shared_ptr<Array> array_a, array_b;
+  ArrayFromVector<TYPE>(type, a, &array_a);
+  ArrayFromVector<TYPE>(type, b, &array_b);
+  auto options = EqualOptions::Defaults().ulp_distance(2);
+
+  // Check with NaN
+  ASSERT_FALSE(array_a->Equals(array_b, options));
+  ASSERT_TRUE(array_a->Equals(array_b, options.nans_equal(true)));
+
+  // Check With Signed Zero
+  ASSERT_FALSE(
+      array_a->Equals(array_b, 
options.nans_equal(true).signed_zeros_equal(false)));
+
+  // Check with Ulp Distance
+  ASSERT_FALSE(array_a->Equals(array_b, 
options.nans_equal(true).ulp_distance(1)));
+}
+
 TEST(TestPrimitiveAdHoc, FloatingApproxEquals) {
   CheckApproxEquals<FloatType>();
   CheckApproxEquals<DoubleType>();
@@ -2446,6 +2490,12 @@ TEST(TestPrimitiveAdHoc, FloatingZeroEquality) {
   CheckFloatingZeroEquality<HalfFloatType>();
 }
 
+TEST(TestPrimitiveAdHoc, FloatingUlpDistanceEquality) {
+  CheckFloatApproxEqualsWithUlpDistance<HalfFloatType>();
+  CheckFloatApproxEqualsWithUlpDistance<FloatType>();
+  CheckFloatApproxEqualsWithUlpDistance<DoubleType>();
+}
+
 // ----------------------------------------------------------------------
 // FixedSizeBinary tests
 
diff --git a/cpp/src/arrow/array/statistics_test.cc 
b/cpp/src/arrow/array/statistics_test.cc
index 62e1ddf831..45535ce823 100644
--- a/cpp/src/arrow/array/statistics_test.cc
+++ b/cpp/src/arrow/array/statistics_test.cc
@@ -255,8 +255,12 @@ TEST_F(TestArrayStatisticsEqualityDoubleValue, NaN) {
 TEST_F(TestArrayStatisticsEqualityDoubleValue, ApproximateEquals) {
   statistics1_.max = 0.5001f;
   statistics2_.max = 0.5;
-  ASSERT_FALSE(statistics1_.Equals(statistics2_, options_.atol(1e-3)));
+  ASSERT_FALSE(statistics1_.Equals(statistics2_, options_));
+  ASSERT_TRUE(statistics1_.Equals(statistics2_, options_.atol(1e-3)));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(statistics1_.Equals(statistics2_, 
options_.atol(1e-3).use_atol(true)));
+  ASSERT_FALSE(statistics1_.Equals(statistics2_, 
options_.atol(1e-3).use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
 }
 
 }  // namespace arrow
diff --git a/cpp/src/arrow/chunked_array.cc b/cpp/src/arrow/chunked_array.cc
index 0fa174c175..edf23e970d 100644
--- a/cpp/src/arrow/chunked_array.cc
+++ b/cpp/src/arrow/chunked_array.cc
@@ -161,7 +161,11 @@ bool ChunkedArray::Equals(const 
std::shared_ptr<ChunkedArray>& other,
 
 bool ChunkedArray::ApproxEquals(const ChunkedArray& other,
                                 const EqualOptions& equal_options) const {
-  return Equals(other, equal_options.use_atol(true));
+  auto resolved_options = equal_options;
+  if (!resolved_options.atol()) {
+    resolved_options = resolved_options.atol(kDefaultAbsoluteTolerance);
+  }
+  return Equals(other, resolved_options);
 }
 
 Result<std::shared_ptr<Scalar>> ChunkedArray::GetScalar(int64_t index) const {
diff --git a/cpp/src/arrow/chunked_array.h b/cpp/src/arrow/chunked_array.h
index 02bcd0f902..2b581d0bb6 100644
--- a/cpp/src/arrow/chunked_array.h
+++ b/cpp/src/arrow/chunked_array.h
@@ -166,6 +166,10 @@ class ARROW_EXPORT ChunkedArray {
   bool Equals(const std::shared_ptr<ChunkedArray>& other,
               const EqualOptions& opts = EqualOptions::Defaults()) const;
   /// \brief Determine if two chunked arrays approximately equal
+  ///
+  /// If the absolute tolerance (atol) is not specified in \ref 
arrow::EqualOptions,
+  /// 'arrow::kDefaultAbsoluteTolerance' is used.
+  ///
   bool ApproxEquals(const ChunkedArray& other,
                     const EqualOptions& = EqualOptions::Defaults()) const;
 
diff --git a/cpp/src/arrow/chunked_array_test.cc 
b/cpp/src/arrow/chunked_array_test.cc
index 326eb24d08..90b90a731b 100644
--- a/cpp/src/arrow/chunked_array_test.cc
+++ b/cpp/src/arrow/chunked_array_test.cc
@@ -202,7 +202,11 @@ TEST_F(TestChunkedArray, ApproxEquals) {
   auto options = EqualOptions::Defaults().atol(1e-3);
 
   ASSERT_FALSE(chunked_array_1->Equals(chunked_array_2));
+  ASSERT_TRUE(chunked_array_1->Equals(chunked_array_2, options));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(chunked_array_1->Equals(chunked_array_2, 
options.use_atol(true)));
+  ASSERT_FALSE(chunked_array_1->Equals(chunked_array_2, 
options.use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
   ASSERT_TRUE(chunked_array_1->ApproxEquals(*chunked_array_2, options));
 }
 
diff --git a/cpp/src/arrow/compare.cc b/cpp/src/arrow/compare.cc
index 26f56d9b58..06f1d14e73 100644
--- a/cpp/src/arrow/compare.cc
+++ b/cpp/src/arrow/compare.cc
@@ -53,6 +53,7 @@
 #include "arrow/util/macros.h"
 #include "arrow/util/memory_internal.h"
 #include "arrow/util/ree_util.h"
+#include "arrow/util/ulp_distance_internal.h"
 #include "arrow/util/unreachable.h"
 #include "arrow/visit_scalar_inline.h"
 #include "arrow/visit_type_inline.h"
@@ -71,39 +72,53 @@ using util::Float16;
 
 namespace {
 
-template <bool Approximate, bool NansEqual, bool SignedZerosEqual>
+template <bool AbsoluteTolerance, bool NansEqual, bool SignedZerosEqual,
+          bool UlpDistanceEqual>
 struct FloatingEqualityFlags {
-  static constexpr bool approximate = Approximate;
+  static constexpr bool absolute_tolerance = AbsoluteTolerance;
   static constexpr bool nans_equal = NansEqual;
   static constexpr bool signed_zeros_equal = SignedZerosEqual;
+  static constexpr bool ulp_distance_equal = UlpDistanceEqual;
 };
 
 template <typename T, typename Flags>
 struct FloatingEquality {
   explicit FloatingEquality(const EqualOptions& options)
-      : epsilon(static_cast<T>(options.atol())) {}
+      : epsilon(static_cast<T>(options.atol().value_or(-1))),
+        ulp_distance(options.ulp_distance().value_or(-1)) {}
 
   bool operator()(T x, T y) const {
     if (x == y) {
       return Flags::signed_zeros_equal || (std::signbit(x) == std::signbit(y));
     }
-    if (Flags::nans_equal && std::isnan(x) && std::isnan(y)) {
+
+    if constexpr (Flags::nans_equal) {
+      if (std::isnan(x) && std::isnan(y)) {
+        return true;
+      }
+    } else if (std::isnan(x) || std::isnan(y)) {
+      return false;
+    }
+
+    if (Flags::absolute_tolerance && (std::fabs(x - y) <= epsilon)) {
       return true;
     }
-    if (Flags::approximate && (fabs(x - y) <= epsilon)) {
+    if (Flags::ulp_distance_equal && internal::WithinUlp(x, y, ulp_distance)) {
       return true;
     }
     return false;
   }
 
   const T epsilon;
+  const int32_t ulp_distance;
 };
 
 // For half-float equality.
 template <typename Flags>
 struct FloatingEquality<uint16_t, Flags> {
   explicit FloatingEquality(const EqualOptions& options)
-      : epsilon(static_cast<float>(options.atol())) {}
+      : epsilon(static_cast<float>(options.atol().value_or(-1))),
+        ulp_distance(options.ulp_distance().value_or(-1)) {}
 
   bool operator()(uint16_t x, uint16_t y) const {
     Float16 f_x = Float16::FromBits(x);
@@ -111,46 +126,65 @@ struct FloatingEquality<uint16_t, Flags> {
     if (f_x == f_y) {
       return Flags::signed_zeros_equal || (f_x.signbit() == f_y.signbit());
     }
-    if (Flags::nans_equal && f_x.is_nan() && f_y.is_nan()) {
+    if constexpr (Flags::nans_equal) {
+      if (f_x.is_nan() && f_y.is_nan()) {
+        return true;
+      }
+    } else if (f_x.is_nan() || f_y.is_nan()) {
+      return false;
+    }
+    if (Flags::absolute_tolerance &&
+        (std::fabs(f_x.ToFloat() - f_y.ToFloat()) <= epsilon)) {
       return true;
     }
-    if (Flags::approximate && (fabs(f_x.ToFloat() - f_y.ToFloat()) <= 
epsilon)) {
+    if (Flags::ulp_distance_equal && internal::WithinUlp(f_x, f_y, 
ulp_distance)) {
       return true;
     }
     return false;
   }
 
   const float epsilon;
+  const int32_t ulp_distance;
 };
 
 template <typename T, typename Visitor>
 struct FloatingEqualityDispatcher {
   const EqualOptions& options;
-  bool floating_approximate;
   Visitor&& visit;
 
-  template <bool Approximate, bool NansEqual>
-  void DispatchL3() {
-    if (options.signed_zeros_equal()) {
-      visit(FloatingEquality<T, FloatingEqualityFlags<Approximate, NansEqual, 
true>>{
+  template <bool AbsoluteTolerance, bool NansEqual, bool SignedZero>
+  void DispatchL4() {
+    if (options.ulp_distance()) {
+      visit(FloatingEquality<
+            T, FloatingEqualityFlags<AbsoluteTolerance, NansEqual, SignedZero, 
true>>{
           options});
     } else {
-      visit(FloatingEquality<T, FloatingEqualityFlags<Approximate, NansEqual, 
false>>{
+      visit(FloatingEquality<
+            T, FloatingEqualityFlags<AbsoluteTolerance, NansEqual, SignedZero, 
false>>{
           options});
     }
   }
 
-  template <bool Approximate>
+  template <bool AbsoluteTolerance, bool NansEqual>
+  void DispatchL3() {
+    if (options.signed_zeros_equal()) {
+      DispatchL4<AbsoluteTolerance, NansEqual, true>();
+    } else {
+      DispatchL4<AbsoluteTolerance, NansEqual, false>();
+    }
+  }
+
+  template <bool AbsoluteTolerance>
   void DispatchL2() {
     if (options.nans_equal()) {
-      DispatchL3<Approximate, true>();
+      DispatchL3<AbsoluteTolerance, true>();
     } else {
-      DispatchL3<Approximate, false>();
+      DispatchL3<AbsoluteTolerance, false>();
     }
   }
 
   void Dispatch() {
-    if (floating_approximate) {
+    if (options.atol()) {
       DispatchL2<true>();
     } else {
       DispatchL2<false>();
@@ -161,10 +195,8 @@ struct FloatingEqualityDispatcher {
 // Call `visit(equality_func)` where `equality_func` has the signature 
`bool(T, T)`
 // and returns true if the two values compare equal.
 template <typename T, typename Visitor>
-void VisitFloatingEquality(const EqualOptions& options, bool 
floating_approximate,
-                           Visitor&& visit) {
-  FloatingEqualityDispatcher<T, Visitor>{options, floating_approximate,
-                                         std::forward<Visitor>(visit)}
+void VisitFloatingEquality(const EqualOptions& options, Visitor&& visit) {
+  FloatingEqualityDispatcher<T, Visitor>{options, std::forward<Visitor>(visit)}
       .Dispatch();
 }
 
@@ -190,20 +222,17 @@ inline bool IdentityImpliesEquality(const DataType& type, 
const EqualOptions& op
 
 bool CompareArrayRanges(const ArrayData& left, const ArrayData& right,
                         int64_t left_start_idx, int64_t left_end_idx,
-                        int64_t right_start_idx, const EqualOptions& options,
-                        bool floating_approximate);
+                        int64_t right_start_idx, const EqualOptions& options);
 
 class RangeDataEqualsImpl {
  public:
   // PRE-CONDITIONS:
   // - the types are equal
   // - the ranges are in bounds
-  RangeDataEqualsImpl(const EqualOptions& options, bool floating_approximate,
-                      const ArrayData& left, const ArrayData& right,
-                      int64_t left_start_idx, int64_t right_start_idx,
-                      int64_t range_length)
+  RangeDataEqualsImpl(const EqualOptions& options, const ArrayData& left,
+                      const ArrayData& right, int64_t left_start_idx,
+                      int64_t right_start_idx, int64_t range_length)
       : options_(options),
-        floating_approximate_(floating_approximate),
         left_(left),
         right_(right),
         left_start_idx_(left_start_idx),
@@ -349,7 +378,7 @@ class RangeDataEqualsImpl {
     const ArrayData& right_data = *right_.child_data[0];
 
     auto compare_runs = [&](int64_t i, int64_t length) -> bool {
-      RangeDataEqualsImpl impl(options_, floating_approximate_, left_data, 
right_data,
+      RangeDataEqualsImpl impl(options_, left_data, right_data,
                                (left_start_idx_ + left_.offset + i) * 
list_size,
                                (right_start_idx_ + right_.offset + i) * 
list_size,
                                length * list_size);
@@ -364,8 +393,7 @@ class RangeDataEqualsImpl {
 
     auto compare_runs = [&](int64_t i, int64_t length) -> bool {
       for (int32_t f = 0; f < num_fields; ++f) {
-        RangeDataEqualsImpl impl(options_, floating_approximate_, 
*left_.child_data[f],
-                                 *right_.child_data[f],
+        RangeDataEqualsImpl impl(options_, *left_.child_data[f], 
*right_.child_data[f],
                                  left_start_idx_ + left_.offset + i,
                                  right_start_idx_ + right_.offset + i, length);
         if (!impl.Compare()) {
@@ -399,11 +427,11 @@ class RangeDataEqualsImpl {
         const auto previous_child_num = child_ids[left_codes[left_start_idx_ + 
i - 1]];
         int64_t run_length = i - run_start;
 
-        RangeDataEqualsImpl impl(
-            options_, floating_approximate_, 
*left_.child_data[previous_child_num],
-            *right_.child_data[previous_child_num],
-            left_start_idx_ + left_.offset + run_start,
-            right_start_idx_ + right_.offset + run_start, run_length);
+        RangeDataEqualsImpl impl(options_, 
*left_.child_data[previous_child_num],
+                                 *right_.child_data[previous_child_num],
+                                 left_start_idx_ + left_.offset + run_start,
+                                 right_start_idx_ + right_.offset + run_start,
+                                 run_length);
 
         if (!impl.Compare()) {
           result_ = false;
@@ -421,7 +449,7 @@ class RangeDataEqualsImpl {
       int64_t final_run_length = range_length_ - run_start;
 
       RangeDataEqualsImpl impl(
-          options_, floating_approximate_, *left_.child_data[final_child_num],
+          options_, *left_.child_data[final_child_num],
           *right_.child_data[final_child_num], left_start_idx_ + left_.offset 
+ run_start,
           right_start_idx_ + right_.offset + run_start, final_run_length);
 
@@ -447,9 +475,8 @@ class RangeDataEqualsImpl {
       }
       const auto child_num = child_ids[type_id];
       RangeDataEqualsImpl impl(
-          options_, floating_approximate_, *left_.child_data[child_num],
-          *right_.child_data[child_num], left_offsets[left_start_idx_ + i],
-          right_offsets[right_start_idx_ + i], 1);
+          options_, *left_.child_data[child_num], 
*right_.child_data[child_num],
+          left_offsets[left_start_idx_ + i], right_offsets[right_start_idx_ + 
i], 1);
       if (!impl.Compare()) {
         result_ = false;
         break;
@@ -464,7 +491,7 @@ class RangeDataEqualsImpl {
         *left_.dictionary, *right_.dictionary,
         /*left_start_idx=*/0,
         /*left_end_idx=*/std::max(left_.dictionary->length, 
right_.dictionary->length),
-        /*right_start_idx=*/0, options_, floating_approximate_);
+        /*right_start_idx=*/0, options_);
     if (result_) {
       // Compare indices
       result_ &= CompareWithType(*type.index_type());
@@ -516,7 +543,7 @@ class RangeDataEqualsImpl {
         return compare_func(x, y);
       });
     };
-    VisitFloatingEquality<CType>(options_, floating_approximate_, 
std::move(visitor));
+    VisitFloatingEquality<CType>(options_, std::move(visitor));
     return Status::OK();
   }
 
@@ -547,8 +574,8 @@ class RangeDataEqualsImpl {
 
     const auto compare_ranges = [&](int64_t left_offset, int64_t right_offset,
                                     int64_t length) -> bool {
-      RangeDataEqualsImpl impl(options_, floating_approximate_, left_data, 
right_data,
-                               left_offset, right_offset, length);
+      RangeDataEqualsImpl impl(options_, left_data, right_data, left_offset, 
right_offset,
+                               length);
       return impl.Compare();
     };
 
@@ -576,8 +603,8 @@ class RangeDataEqualsImpl {
         if (size == 0) {
           continue;
         }
-        RangeDataEqualsImpl impl(options_, floating_approximate_, left_values,
-                                 right_values, left_offsets[j], 
right_offsets[j], size);
+        RangeDataEqualsImpl impl(options_, left_values, right_values, 
left_offsets[j],
+                                 right_offsets[j], size);
         if (!impl.Compare()) {
           return false;
         }
@@ -602,7 +629,7 @@ class RangeDataEqualsImpl {
 
     auto it = ree_util::MergedRunsIterator(left, right);
     for (; !it.is_end(); ++it) {
-      RangeDataEqualsImpl impl(options_, floating_approximate_, left_values, 
right_values,
+      RangeDataEqualsImpl impl(options_, left_values, right_values,
                                it.index_into_left_array(), 
it.index_into_right_array(),
                                /*range_length=*/1);
       if (!impl.Compare()) {
@@ -670,7 +697,6 @@ class RangeDataEqualsImpl {
   }
 
   const EqualOptions& options_;
-  const bool floating_approximate_;
   const ArrayData& left_;
   const ArrayData& right_;
   const int64_t left_start_idx_;
@@ -682,8 +708,7 @@ class RangeDataEqualsImpl {
 
 bool CompareArrayRanges(const ArrayData& left, const ArrayData& right,
                         int64_t left_start_idx, int64_t left_end_idx,
-                        int64_t right_start_idx, const EqualOptions& options,
-                        bool floating_approximate) {
+                        int64_t right_start_idx, const EqualOptions& options) {
   if (left.type->id() != right.type->id() ||
       !TypeEquals(*left.type, *right.type, false /* check_metadata */)) {
     return false;
@@ -704,8 +729,8 @@ bool CompareArrayRanges(const ArrayData& left, const 
ArrayData& right,
     return true;
   }
   // Compare values
-  RangeDataEqualsImpl impl(options, floating_approximate, left, right, 
left_start_idx,
-                           right_start_idx, range_length);
+  RangeDataEqualsImpl impl(options, left, right, left_start_idx, 
right_start_idx,
+                           range_length);
   return impl.Compare();
 }
 
@@ -875,22 +900,13 @@ class TypeEqualsVisitor {
   bool result_;
 };
 
-bool ArrayEquals(const Array& left, const Array& right, const EqualOptions& 
opts,
-                 bool floating_approximate);
-bool ScalarEquals(const Scalar& left, const Scalar& right, const EqualOptions& 
options,
-                  bool floating_approximate);
-
 class ScalarEqualsVisitor {
  public:
   // PRE-CONDITIONS:
   // - the types are equal
   // - the scalars are non-null
-  explicit ScalarEqualsVisitor(const Scalar& right, const EqualOptions& opts,
-                               bool floating_approximate)
-      : right_(right),
-        options_(opts),
-        floating_approximate_(floating_approximate),
-        result_(false) {}
+  explicit ScalarEqualsVisitor(const Scalar& right, const EqualOptions& opts)
+      : right_(right), options_(opts), result_(false) {}
 
   Status Visit(const NullScalar& left) {
     result_ = true;
@@ -952,37 +968,37 @@ class ScalarEqualsVisitor {
 
   Status Visit(const ListScalar& left) {
     const auto& right = checked_cast<const ListScalar&>(right_);
-    result_ = ArrayEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ArrayEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const LargeListScalar& left) {
     const auto& right = checked_cast<const LargeListScalar&>(right_);
-    result_ = ArrayEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ArrayEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const ListViewScalar& left) {
     const auto& right = checked_cast<const ListViewScalar&>(right_);
-    result_ = ArrayEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ArrayEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const LargeListViewScalar& left) {
     const auto& right = checked_cast<const LargeListViewScalar&>(right_);
-    result_ = ArrayEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ArrayEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const MapScalar& left) {
     const auto& right = checked_cast<const MapScalar&>(right_);
-    result_ = ArrayEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ArrayEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const FixedSizeListScalar& left) {
     const auto& right = checked_cast<const FixedSizeListScalar&>(right_);
-    result_ = ArrayEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ArrayEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
@@ -994,8 +1010,7 @@ class ScalarEqualsVisitor {
     } else {
       bool all_equals = true;
       for (size_t i = 0; i < left.value.size() && all_equals; i++) {
-        all_equals &= ScalarEquals(*left.value[i], *right.value[i], options_,
-                                   floating_approximate_);
+        all_equals &= ScalarEquals(*left.value[i], *right.value[i], options_);
       }
       result_ = all_equals;
     }
@@ -1005,35 +1020,33 @@ class ScalarEqualsVisitor {
 
   Status Visit(const DenseUnionScalar& left) {
     const auto& right = checked_cast<const DenseUnionScalar&>(right_);
-    result_ = ScalarEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ScalarEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const SparseUnionScalar& left) {
     const auto& right = checked_cast<const SparseUnionScalar&>(right_);
-    result_ = ScalarEquals(*left.value[left.child_id], 
*right.value[right.child_id],
-                           options_, floating_approximate_);
+    result_ =
+        ScalarEquals(*left.value[left.child_id], *right.value[right.child_id], 
options_);
     return Status::OK();
   }
 
   Status Visit(const DictionaryScalar& left) {
     const auto& right = checked_cast<const DictionaryScalar&>(right_);
-    result_ = ScalarEquals(*left.value.index, *right.value.index, options_,
-                           floating_approximate_) &&
-              ArrayEquals(*left.value.dictionary, *right.value.dictionary, 
options_,
-                          floating_approximate_);
+    result_ = ScalarEquals(*left.value.index, *right.value.index, options_) &&
+              ArrayEquals(*left.value.dictionary, *right.value.dictionary, 
options_);
     return Status::OK();
   }
 
   Status Visit(const RunEndEncodedScalar& left) {
     const auto& right = checked_cast<const RunEndEncodedScalar&>(right_);
-    result_ = ScalarEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ScalarEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
   Status Visit(const ExtensionScalar& left) {
     const auto& right = checked_cast<const ExtensionScalar&>(right_);
-    result_ = ScalarEquals(*left.value, *right.value, options_, 
floating_approximate_);
+    result_ = ScalarEquals(*left.value, *right.value, options_);
     return Status::OK();
   }
 
@@ -1048,13 +1061,12 @@ class ScalarEqualsVisitor {
     auto visitor = [&](auto&& compare_func) {
       result_ = compare_func(left.value, right.value);
     };
-    VisitFloatingEquality<CType>(options_, floating_approximate_, 
std::move(visitor));
+    VisitFloatingEquality<CType>(options_, std::move(visitor));
     return Status::OK();
   }
 
   const Scalar& right_;
   const EqualOptions options_;
-  const bool floating_approximate_;
   bool result_;
 };
 
@@ -1107,12 +1119,13 @@ Status PrintDiff(const Array& left, const Array& right, 
std::ostream* os) {
   return PrintDiff(left, right, 0, left.length(), 0, right.length(), os);
 }
 
+}  // namespace
+
 bool ArrayRangeEquals(const Array& left, const Array& right, int64_t 
left_start_idx,
                       int64_t left_end_idx, int64_t right_start_idx,
-                      const EqualOptions& options, bool floating_approximate) {
-  bool are_equal =
-      CompareArrayRanges(*left.data(), *right.data(), left_start_idx, 
left_end_idx,
-                         right_start_idx, options, floating_approximate);
+                      const EqualOptions& options) {
+  bool are_equal = CompareArrayRanges(*left.data(), *right.data(), 
left_start_idx,
+                                      left_end_idx, right_start_idx, options);
   if (!are_equal) {
     ARROW_IGNORE_EXPR(PrintDiff(
         left, right, left_start_idx, left_end_idx, right_start_idx,
@@ -1121,17 +1134,34 @@ bool ArrayRangeEquals(const Array& left, const Array& 
right, int64_t left_start_
   return are_equal;
 }
 
-bool ArrayEquals(const Array& left, const Array& right, const EqualOptions& 
opts,
-                 bool floating_approximate) {
+bool ArrayRangeApproxEquals(const Array& left, const Array& right, int64_t 
left_start_idx,
+                            int64_t left_end_idx, int64_t right_start_idx,
+                            const EqualOptions& options) {
+  auto resolved_options = options;
+  if (!resolved_options.atol()) {
+    resolved_options = options.atol(kDefaultAbsoluteTolerance);
+  }
+  return ArrayRangeEquals(left, right, left_start_idx, left_end_idx, 
right_start_idx,
+                          resolved_options);
+}
+
+bool ArrayEquals(const Array& left, const Array& right, const EqualOptions& 
opts) {
   if (left.length() != right.length()) {
     ARROW_IGNORE_EXPR(PrintDiff(left, right, opts.diff_sink()));
     return false;
   }
-  return ArrayRangeEquals(left, right, 0, left.length(), 0, opts, 
floating_approximate);
+  return ArrayRangeEquals(left, right, 0, left.length(), 0, opts);
+}
+
+bool ArrayApproxEquals(const Array& left, const Array& right, const 
EqualOptions& opts) {
+  auto resolved_options = opts;
+  if (!resolved_options.atol()) {
+    resolved_options = resolved_options.atol(kDefaultAbsoluteTolerance);
+  }
+  return ArrayEquals(left, right, resolved_options);
 }
 
-bool ScalarEquals(const Scalar& left, const Scalar& right, const EqualOptions& 
options,
-                  bool floating_approximate) {
+bool ScalarEquals(const Scalar& left, const Scalar& right, const EqualOptions& 
options) {
   if (&left == &right && IdentityImpliesEquality(*left.type, options)) {
     return true;
   }
@@ -1144,46 +1174,19 @@ bool ScalarEquals(const Scalar& left, const Scalar& 
right, const EqualOptions& o
   if (!left.is_valid) {
     return true;
   }
-  ScalarEqualsVisitor visitor(right, options, floating_approximate);
+  ScalarEqualsVisitor visitor(right, options);
   auto error = VisitScalarInline(left, &visitor);
   DCHECK_OK(error);
   return visitor.result();
 }
 
-}  // namespace
-
-bool ArrayRangeEquals(const Array& left, const Array& right, int64_t 
left_start_idx,
-                      int64_t left_end_idx, int64_t right_start_idx,
-                      const EqualOptions& options) {
-  return ArrayRangeEquals(left, right, left_start_idx, left_end_idx, 
right_start_idx,
-                          options, options.use_atol());
-}
-
-bool ArrayRangeApproxEquals(const Array& left, const Array& right, int64_t 
left_start_idx,
-                            int64_t left_end_idx, int64_t right_start_idx,
-                            const EqualOptions& options) {
-  const bool floating_approximate = true;
-  return ArrayRangeEquals(left, right, left_start_idx, left_end_idx, 
right_start_idx,
-                          options, floating_approximate);
-}
-
-bool ArrayEquals(const Array& left, const Array& right, const EqualOptions& 
opts) {
-  return ArrayEquals(left, right, opts, opts.use_atol());
-}
-
-bool ArrayApproxEquals(const Array& left, const Array& right, const 
EqualOptions& opts) {
-  const bool floating_approximate = true;
-  return ArrayEquals(left, right, opts, floating_approximate);
-}
-
-bool ScalarEquals(const Scalar& left, const Scalar& right, const EqualOptions& 
options) {
-  return ScalarEquals(left, right, options, options.use_atol());
-}
-
 bool ScalarApproxEquals(const Scalar& left, const Scalar& right,
                         const EqualOptions& options) {
-  const bool floating_approximate = true;
-  return ScalarEquals(left, right, options, floating_approximate);
+  auto resolved_options = options;
+  if (!options.atol()) {
+    resolved_options = options.atol(kDefaultAbsoluteTolerance);
+  }
+  return ScalarEquals(left, right, resolved_options);
 }
 
 namespace {
@@ -1274,8 +1277,7 @@ bool StridedFloatTensorContentEquals(const int dim_index, 
int64_t left_offset,
       }
     };
 
-    VisitFloatingEquality<c_type>(opts, /*floating_approximate=*/false,
-                                  std::move(visitor));
+    VisitFloatingEquality<c_type>(opts, std::move(visitor));
     return result;
   }
 
@@ -1528,7 +1530,7 @@ namespace {
 bool DoubleEquals(const double& left, const double& right, const EqualOptions& 
options) {
   bool result;
   auto visitor = [&](auto&& compare_func) { result = compare_func(left, 
right); };
-  VisitFloatingEquality<double>(options, options.use_atol(), 
std::move(visitor));
+  VisitFloatingEquality<double>(options, std::move(visitor));
   return result;
 }
 
diff --git a/cpp/src/arrow/compare.h b/cpp/src/arrow/compare.h
index 2198495d7d..27b82c34c4 100644
--- a/cpp/src/arrow/compare.h
+++ b/cpp/src/arrow/compare.h
@@ -21,6 +21,7 @@
 
 #include <cstdint>
 #include <iosfwd>
+#include <optional>
 
 #include "arrow/util/macros.h"
 #include "arrow/util/visibility.h"
@@ -35,6 +36,7 @@ class SparseTensor;
 struct Scalar;
 
 static constexpr double kDefaultAbsoluteTolerance = 1E-5;
+static constexpr int32_t kDefaultUlpDistance = 4;
 
 /// A container of options for equality comparisons
 class EqualOptions {
@@ -63,26 +65,49 @@ class EqualOptions {
   ///
   /// This option only affects the Equals methods
   /// and has no effect on ApproxEquals methods.
-  bool use_atol() const { return use_atol_; }
+  /// \deprecated Deprecated in 25.0.0. Use arrow::EqualOptions::atol instead
+  ARROW_DEPRECATED("Deprecated in 25.0.0. Use arrow::EqualOptions::atol 
instead")
+  bool use_atol() const { return atol_.has_value(); }
 
   /// Return a new EqualOptions object with the "use_atol" property changed.
+  /// \deprecated Deprecated in 25.0.0. Use arrow::EqualOptions::atol instead
+  ARROW_DEPRECATED("Deprecated in 25.0.0. Use arrow::EqualOptions::atol 
instead")
   EqualOptions use_atol(bool v) const {
     auto res = EqualOptions(*this);
-    res.use_atol_ = v;
+    if (v) {
+      res.atol_ = atol_.value_or(kDefaultAbsoluteTolerance);
+    } else {
+      res.atol_ = std::nullopt;
+    }
     return res;
   }
 
   /// The absolute tolerance for approximate comparisons of floating-point 
values.
-  /// Note that this option is ignored if "use_atol" is set to false.
-  double atol() const { return atol_; }
+  std::optional<double> atol() const { return atol_; }
 
   /// Return a new EqualOptions object with the "atol" property changed.
-  EqualOptions atol(double v) const {
+  /// If both "ulp_distance" and "atol" are specified, the comparison
+  /// succeeds when the "ulp_distance" condition OR the "atol" condition
+  /// is satisfied.
+  EqualOptions atol(std::optional<double> v) const {
     auto res = EqualOptions(*this);
     res.atol_ = v;
     return res;
   }
 
+  /// The ulp distance for approximate comparisons of floating-point values.
+  std::optional<int32_t> ulp_distance() const { return ulp_distance_; }
+
+  /// Return a new EqualOptions object with the "ulp_distance" property 
changed.
+  /// If both "ulp_distance" and "atol" are specified, the comparison
+  /// succeeds when the "ulp_distance" condition OR the "atol" condition
+  /// is satisfied.
+  EqualOptions ulp_distance(std::optional<int32_t> v) const {
+    auto res = EqualOptions(*this);
+    res.ulp_distance_ = v;
+    return res;
+  }
+
   /// Whether the \ref arrow::Schema property is used in the comparison.
   ///
   /// This option only affects the Equals methods
@@ -131,10 +156,10 @@ class EqualOptions {
   static EqualOptions Defaults() { return {}; }
 
  protected:
-  double atol_ = kDefaultAbsoluteTolerance;
+  std::optional<double> atol_ = std::nullopt;
+  std::optional<int32_t> ulp_distance_ = std::nullopt;
   bool nans_equal_ = false;
   bool signed_zeros_equal_ = true;
-  bool use_atol_ = false;
   bool use_schema_ = true;
   bool use_metadata_ = false;
 
@@ -150,6 +175,9 @@ ARROW_EXPORT bool ArrayEquals(const Array& left, const 
Array& right,
 /// Returns true if the arrays are approximately equal. For non-floating point
 /// types, this is equivalent to ArrayEquals(left, right)
 ///
+/// If the absolute tolerance (atol) is not specified in \ref 
arrow::EqualOptions,
+/// 'arrow::kDefaultAbsoluteTolerance' is used.
+///
 /// Note that arrow::ArrayStatistics is not included in the comparison.
 ARROW_EXPORT bool ArrayApproxEquals(const Array& left, const Array& right,
                                     const EqualOptions& = 
EqualOptions::Defaults());
@@ -164,6 +192,9 @@ ARROW_EXPORT bool ArrayRangeEquals(const Array& left, const 
Array& right,
 
 /// Returns true if indicated equal-length segment of arrays are approximately 
equal
 ///
+/// If the absolute tolerance (atol) is not specified in \ref 
arrow::EqualOptions,
+/// 'arrow::kDefaultAbsoluteTolerance' is used.
+///
 /// Note that arrow::ArrayStatistics is not included in the comparison.
 ARROW_EXPORT bool ArrayRangeApproxEquals(const Array& left, const Array& right,
                                          int64_t start_idx, int64_t end_idx,
@@ -203,6 +234,10 @@ ARROW_EXPORT bool ScalarEquals(const Scalar& left, const 
Scalar& right,
                                const EqualOptions& options = 
EqualOptions::Defaults());
 
 /// Returns true if scalars are approximately equal
+///
+/// If the absolute tolerance (atol) is not specified in \ref 
arrow::EqualOptions,
+/// 'arrow::kDefaultAbsoluteTolerance' is used.
+///
 /// \param[in] left a Scalar
 /// \param[in] right a Scalar
 /// \param[in] options comparison options
diff --git a/cpp/src/arrow/meson.build b/cpp/src/arrow/meson.build
index 4b8faebecf..831bc12180 100644
--- a/cpp/src/arrow/meson.build
+++ b/cpp/src/arrow/meson.build
@@ -214,6 +214,7 @@ arrow_util_srcs = [
     'util/time.cc',
     'util/tracing.cc',
     'util/trie.cc',
+    'util/ulp_distance.cc',
     'util/union_util.cc',
     'util/unreachable.cc',
     'util/uri.cc',
diff --git a/cpp/src/arrow/record_batch.h b/cpp/src/arrow/record_batch.h
index 0d1d2d4ac3..17d7f9857a 100644
--- a/cpp/src/arrow/record_batch.h
+++ b/cpp/src/arrow/record_batch.h
@@ -137,12 +137,19 @@ class ARROW_EXPORT RecordBatch {
 
   /// \brief Determine if two record batches are approximately equal
   ///
+  /// If the absolute tolerance (atol) is not specified in \ref 
arrow::EqualOptions,
+  /// 'arrow::kDefaultAbsoluteTolerance' is used.
+  ///
   /// \param[in] other the RecordBatch to compare with
   /// \param[in] opts the options for equality comparisons
   /// \return true if batches are approximately equal
   bool ApproxEquals(const RecordBatch& other,
                     const EqualOptions& opts = EqualOptions::Defaults()) const 
{
-    return Equals(other, opts.use_schema(false).use_atol(true));
+    auto resolved_options = opts.use_schema(false);
+    if (!resolved_options.atol()) {
+      resolved_options = resolved_options.atol(kDefaultAbsoluteTolerance);
+    }
+    return Equals(other, resolved_options);
   }
 
   /// \return the record batch's schema
diff --git a/cpp/src/arrow/record_batch_test.cc 
b/cpp/src/arrow/record_batch_test.cc
index a037d7261e..904285fd1c 100644
--- a/cpp/src/arrow/record_batch_test.cc
+++ b/cpp/src/arrow/record_batch_test.cc
@@ -208,8 +208,12 @@ TEST_F(TestRecordBatchEqualOptions, Approx) {
   EXPECT_FALSE(b1->ApproxEquals(*b2, 
EqualOptions::Defaults().nans_equal(true)));
 
   auto options = EqualOptions::Defaults().nans_equal(true).atol(0.1);
-  EXPECT_FALSE(b1->Equals(*b2, options));
+  EXPECT_FALSE(b1->Equals(*b2));
+  EXPECT_TRUE(b1->Equals(*b2, options));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
   EXPECT_TRUE(b1->Equals(*b2, options.use_atol(true)));
+  EXPECT_FALSE(b1->Equals(*b2, options.use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
   EXPECT_TRUE(b1->ApproxEquals(*b2, options));
 }
 
diff --git a/cpp/src/arrow/scalar_test.cc b/cpp/src/arrow/scalar_test.cc
index 4a34e5d13c..4096b8cd17 100644
--- a/cpp/src/arrow/scalar_test.cc
+++ b/cpp/src/arrow/scalar_test.cc
@@ -20,11 +20,13 @@
 #include <memory>
 #include <ostream>
 #include <string>
+#include <type_traits>
 #include <unordered_set>
 #include <utility>
 #include <vector>
 
 #include <gmock/gmock.h>
+#include <gtest/gtest-spi.h>
 #include <gtest/gtest.h>
 
 #include "arrow/array.h"
@@ -408,8 +410,12 @@ class TestRealScalar : public ::testing::Test {
   void TestUseAtol() {
     auto options = EqualOptions::Defaults().atol(0.2f);
 
-    ASSERT_FALSE(scalar_val_->Equals(*scalar_other_, options));
+    ASSERT_FALSE(scalar_val_->Equals(*scalar_other_));
+    ASSERT_TRUE(scalar_val_->Equals(*scalar_other_, options));
+    ARROW_SUPPRESS_DEPRECATION_WARNING
     ASSERT_TRUE(scalar_val_->Equals(*scalar_other_, options.use_atol(true)));
+    ASSERT_FALSE(scalar_val_->Equals(*scalar_other_, options.use_atol(false)));
+    ARROW_UNSUPPRESS_DEPRECATION_WARNING
     ASSERT_TRUE(scalar_val_->ApproxEquals(*scalar_other_, options));
   }
 
@@ -434,8 +440,8 @@ class TestRealScalar : public ::testing::Test {
     ASSERT_FALSE(struct_nan.ApproxEquals(struct_other_nan, options));
 
     options = options.atol(0.15);
-    ASSERT_FALSE(struct_val.Equals(struct_other_val, options));
-    ASSERT_FALSE(struct_other_val.Equals(struct_val, options));
+    ASSERT_TRUE(struct_val.Equals(struct_other_val, options));
+    ASSERT_TRUE(struct_other_val.Equals(struct_val, options));
     ASSERT_FALSE(struct_nan.Equals(struct_val, options));
     ASSERT_FALSE(struct_nan.Equals(struct_nan, options));
     ASSERT_FALSE(struct_nan.Equals(struct_other_nan, options));
@@ -446,8 +452,8 @@ class TestRealScalar : public ::testing::Test {
     ASSERT_FALSE(struct_nan.ApproxEquals(struct_other_nan, options));
 
     options = options.nans_equal(true);
-    ASSERT_FALSE(struct_val.Equals(struct_other_val, options));
-    ASSERT_FALSE(struct_other_val.Equals(struct_val, options));
+    ASSERT_TRUE(struct_val.Equals(struct_other_val, options));
+    ASSERT_TRUE(struct_other_val.Equals(struct_val, options));
     ASSERT_FALSE(struct_nan.Equals(struct_val, options));
     ASSERT_TRUE(struct_nan.Equals(struct_nan, options));
     ASSERT_TRUE(struct_nan.Equals(struct_other_nan, options));
@@ -491,7 +497,7 @@ class TestRealScalar : public ::testing::Test {
 
     options = options.atol(0.15);
     ASSERT_TRUE(list_val.Equals(list_val, options));
-    ASSERT_FALSE(list_val.Equals(list_other_val, options));
+    ASSERT_TRUE(list_val.Equals(list_other_val, options));
     ASSERT_FALSE(list_nan.Equals(list_val, options));
     ASSERT_FALSE(list_nan.Equals(list_nan, options));
     ASSERT_FALSE(list_nan.Equals(list_other_nan, options));
@@ -503,7 +509,7 @@ class TestRealScalar : public ::testing::Test {
 
     options = options.nans_equal(true);
     ASSERT_TRUE(list_val.Equals(list_val, options));
-    ASSERT_FALSE(list_val.Equals(list_other_val, options));
+    ASSERT_TRUE(list_val.Equals(list_other_val, options));
     ASSERT_FALSE(list_nan.Equals(list_val, options));
     ASSERT_TRUE(list_nan.Equals(list_nan, options));
     ASSERT_TRUE(list_nan.Equals(list_other_nan, options));
@@ -562,6 +568,75 @@ TYPED_TEST(TestRealScalar, ListViewOf) { 
this->TestListViewOf(); }
 
 TYPED_TEST(TestRealScalar, LargeListViewOf) { this->TestLargeListViewOf(); }
 
+namespace {
+
+template <typename CType>
+void AssertScalarsEqual(const CType& left, const CType& right,
+                        const EqualOptions& options) {
+  using ScalarType = TypeTraits<typename 
CTypeTraits<CType>::ArrowType>::ScalarType;
+  arrow::AssertScalarsEqual(ScalarType(left), ScalarType(right), false, 
options);
+}
+
+}  // namespace
+
+TEST(TestRealScalarUlpDistance, Double) {
+  // 'static' ensures the variable outlives EXPECT_FATAL_FAILURE's separate 
execution
+  // context.
+  static auto options = EqualOptions::Defaults();
+  AssertScalarsEqual(0.9999999999999988, 1.0000000000000007, 
options.ulp_distance(14));
+#ifndef _WIN32
+  // GH-47442
+  EXPECT_FATAL_FAILURE(AssertScalarsEqual(0.9999999999999988, 
1.0000000000000007,
+                                          options.ulp_distance(13)),
+                       "");
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(0.9999999999999988, 
std::numeric_limits<double>::quiet_NaN(),
+                         options.ulp_distance(14).nans_equal(true)),
+      "");
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(0.9999999999999988, 
std::numeric_limits<double>::quiet_NaN(),
+                         options.ulp_distance(14).nans_equal(false)),
+      "");
+#endif
+}
+
+TEST(TestRealScalarUlpDistance, Float) {
+  static auto options = EqualOptions::Defaults();
+  AssertScalarsEqual(123.456f, 123.456085f, options.ulp_distance(11));
+#ifndef _WIN32
+  // GH-47442
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(123.456f, 123.456085f, options.ulp_distance(10)), "");
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(123.456f, std::numeric_limits<float>::quiet_NaN(),
+                         options.ulp_distance(11).nans_equal(true)),
+      "");
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(123.456f, std::numeric_limits<float>::quiet_NaN(),
+                         options.ulp_distance(11).nans_equal(false)),
+      "");
+#endif
+}
+
+TEST(TestRealScalarUlpDistance, HalfFloat) {
+  static auto options = EqualOptions::Defaults();
+  AssertScalarsEqual(Float16(1.00097656), Float16(0.999511719f), 
options.ulp_distance(2));
+#ifndef _WIN32
+  // GH-47442
+  EXPECT_FATAL_FAILURE(AssertScalarsEqual(Float16(1.00097656), 
Float16(0.999511719f),
+                                          options.ulp_distance(1)),
+                       "");
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(Float16(1.00097656), 
std::numeric_limits<Float16>::quiet_NaN(),
+                         options.ulp_distance(2).nans_equal(true)),
+      "");
+  EXPECT_FATAL_FAILURE(
+      AssertScalarsEqual(Float16(1.00097656), 
std::numeric_limits<Float16>::quiet_NaN(),
+                         options.ulp_distance(2).nans_equal(false)),
+      "");
+#endif
+}
+
 template <typename T>
 class TestDecimalScalar : public ::testing::Test {
  public:
diff --git a/cpp/src/arrow/table_test.cc b/cpp/src/arrow/table_test.cc
index 692671910b..4182bf020a 100644
--- a/cpp/src/arrow/table_test.cc
+++ b/cpp/src/arrow/table_test.cc
@@ -306,9 +306,17 @@ TEST(TestTableEqualityFloatType, Approximate) {
 
   ASSERT_FALSE(table->Equals(*other_table, options));
 
-  ASSERT_TRUE(table->Equals(*other_table, options.use_atol(true).atol(1e-3)));
-
-  ASSERT_FALSE(table->Equals(*other_table, options.use_atol(true).atol(1e-5)));
+  ASSERT_TRUE(table->Equals(*other_table, options.atol(1e-3)));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
+  ASSERT_TRUE(table->Equals(*other_table, options.atol(1e-3).use_atol(true)));
+  ASSERT_FALSE(table->Equals(*other_table, 
options.atol(1e-3).use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
+
+  ASSERT_FALSE(table->Equals(*other_table, options.atol(1e-5)));
+  ARROW_SUPPRESS_DEPRECATION_WARNING
+  ASSERT_FALSE(table->Equals(*other_table, options.use_atol(true)));
+  ASSERT_FALSE(table->Equals(*other_table, options.use_atol(false)));
+  ARROW_UNSUPPRESS_DEPRECATION_WARNING
 }
 
 TEST(TestTableEqualitySameAddress, NonFloatType) {
diff --git a/cpp/src/arrow/testing/gtest_util_test.cc 
b/cpp/src/arrow/testing/gtest_util_test.cc
index 2a28df79dd..31b5b9e662 100644
--- a/cpp/src/arrow/testing/gtest_util_test.cc
+++ b/cpp/src/arrow/testing/gtest_util_test.cc
@@ -17,7 +17,6 @@
 
 #include <cmath>
 #include <memory>
-#include <type_traits>
 #include <vector>
 
 #include <gtest/gtest-spi.h>
@@ -180,187 +179,16 @@ TEST_F(TestTensorFromJSON, FromJSON) {
   EXPECT_TRUE(tensor_expected->Equals(*result));
 }
 
-template <typename Float>
-void CheckWithinUlpSingle(Float x, Float y, int n_ulp) {
-  ARROW_SCOPED_TRACE("x = ", x, ", y = ", y, ", n_ulp = ", n_ulp);
-  ASSERT_TRUE(WithinUlp(x, y, n_ulp));
-}
-
-template <typename Float>
-void CheckNotWithinUlpSingle(Float x, Float y, int n_ulp) {
-  ARROW_SCOPED_TRACE("x = ", x, ", y = ", y, ", n_ulp = ", n_ulp);
-  ASSERT_FALSE(WithinUlp(x, y, n_ulp));
-}
-
-template <typename Float>
-void CheckWithinUlp(Float x, Float y, int n_ulp) {
-  CheckWithinUlpSingle(x, y, n_ulp);
-  CheckWithinUlpSingle(y, x, n_ulp);
-  CheckWithinUlpSingle(x, y, n_ulp + 1);
-  CheckWithinUlpSingle(y, x, n_ulp + 1);
-  CheckWithinUlpSingle(-x, -y, n_ulp);
-  CheckWithinUlpSingle(-y, -x, n_ulp);
-
-  for (int exp : {1, -1, 10, -10}) {
-    Float x_scaled(0);
-    Float y_scaled(0);
-    if constexpr (std::is_same_v<Float, Float16>) {
-      x_scaled = Float16(std::ldexp(x.ToFloat(), exp));
-      y_scaled = Float16(std::ldexp(y.ToFloat(), exp));
-    } else {
-      x_scaled = std::ldexp(x, exp);
-      y_scaled = std::ldexp(y, exp);
-    }
-    CheckWithinUlpSingle(x_scaled, y_scaled, n_ulp);
-    CheckWithinUlpSingle(y_scaled, x_scaled, n_ulp);
-  }
-}
-
-template <typename Float>
-void CheckNotWithinUlp(Float x, Float y, int n_ulp) {
-  CheckNotWithinUlpSingle(x, y, n_ulp);
-  CheckNotWithinUlpSingle(y, x, n_ulp);
-  CheckNotWithinUlpSingle(-x, -y, n_ulp);
-  CheckNotWithinUlpSingle(-y, -x, n_ulp);
-  if (n_ulp > 1) {
-    CheckNotWithinUlpSingle(x, y, n_ulp - 1);
-    CheckNotWithinUlpSingle(y, x, n_ulp - 1);
-    CheckNotWithinUlpSingle(-x, -y, n_ulp - 1);
-    CheckNotWithinUlpSingle(-y, -x, n_ulp - 1);
-  }
-
-  for (int exp : {1, -1, 10, -10}) {
-    Float x_scaled(0);
-    Float y_scaled(0);
-    if constexpr (std::is_same_v<Float, Float16>) {
-      x_scaled = Float16(std::ldexp(x.ToFloat(), exp));
-      y_scaled = Float16(std::ldexp(y.ToFloat(), exp));
-    } else {
-      x_scaled = std::ldexp(x, exp);
-      y_scaled = std::ldexp(y, exp);
-    }
-    CheckNotWithinUlpSingle(x_scaled, y_scaled, n_ulp);
-    CheckNotWithinUlpSingle(y_scaled, x_scaled, n_ulp);
-  }
-}
-
-TEST(TestWithinUlp, Double) {
-  for (double f : {0.0, 1e-20, 1.0, 2345678.9}) {
-    CheckWithinUlp(f, f, 0);
-    CheckWithinUlp(f, f, 1);
-    CheckWithinUlp(f, f, 42);
-  }
-  CheckWithinUlp(-0.0, 0.0, 1);
-  CheckWithinUlp(1.0, 1.0000000000000002, 1);
-  CheckWithinUlp(1.0, 1.0000000000000007, 3);
-  CheckNotWithinUlp(1.0, 1.0000000000000002, 0);
-  CheckNotWithinUlp(1.0, 1.0000000000000007, 2);
-  CheckNotWithinUlp(1.0, 1.0000000000000007, 1);
-  // left and right have a different exponent but are still very close
-  CheckWithinUlp(1.0, 0.9999999999999999, 1);
-  CheckWithinUlp(1.0, 0.9999999999999988, 11);
-  CheckNotWithinUlp(1.0, 0.9999999999999988, 10);
-  CheckWithinUlp(1.0000000000000002, 0.9999999999999999, 2);
-  CheckNotWithinUlp(1.0000000000000002, 0.9999999999999999, 1);
-  CheckWithinUlp(0.9999999999999988, 1.0000000000000007, 14);
-  CheckNotWithinUlp(0.9999999999999988, 1.0000000000000007, 13);
-
-  CheckWithinUlp(123.4567, 123.45670000000015, 11);
-  CheckNotWithinUlp(123.4567, 123.45670000000015, 10);
-
-  CheckWithinUlp(HUGE_VAL, HUGE_VAL, 10);
-  CheckWithinUlp(-HUGE_VAL, -HUGE_VAL, 10);
-  CheckWithinUlp(std::nan(""), std::nan(""), 10);
-  CheckNotWithinUlp(HUGE_VAL, -HUGE_VAL, 10);
-  CheckNotWithinUlp(12.34, -HUGE_VAL, 10);
-  CheckNotWithinUlp(12.34, std::nan(""), 10);
-  CheckNotWithinUlp(12.34, -12.34, 10);
-  CheckNotWithinUlp(0.0, 1e-20, 10);
-}
-
-TEST(TestWithinUlp, Float) {
-  for (float f : {0.0f, 1e-8f, 1.0f, 123.456f}) {
-    CheckWithinUlp(f, f, 0);
-    CheckWithinUlp(f, f, 1);
-    CheckWithinUlp(f, f, 42);
-  }
-  CheckWithinUlp(-0.0f, 0.0f, 1);
-  CheckWithinUlp(1.0f, 1.0000001f, 1);
-  CheckWithinUlp(1.0f, 1.0000013f, 11);
-  CheckNotWithinUlp(1.0f, 1.0000001f, 0);
-  CheckNotWithinUlp(1.0f, 1.0000013f, 10);
-  // left and right have a different exponent but are still very close
-  CheckWithinUlp(1.0f, 0.99999994f, 1);
-  CheckWithinUlp(1.0f, 0.99999934f, 11);
-  CheckNotWithinUlp(1.0f, 0.99999934f, 10);
-  CheckWithinUlp(1.0000001f, 0.99999994f, 2);
-  CheckNotWithinUlp(1.0000001f, 0.99999994f, 1);
-  CheckWithinUlp(1.0000013f, 0.99999934f, 22);
-  CheckNotWithinUlp(1.0000013f, 0.99999934f, 21);
-
-  CheckWithinUlp(123.456f, 123.456085f, 11);
-  CheckNotWithinUlp(123.456f, 123.456085f, 10);
-
-  CheckWithinUlp(HUGE_VALF, HUGE_VALF, 10);
-  CheckWithinUlp(-HUGE_VALF, -HUGE_VALF, 10);
-  CheckWithinUlp(std::nanf(""), std::nanf(""), 10);
-  CheckNotWithinUlp(HUGE_VALF, -HUGE_VALF, 10);
-  CheckNotWithinUlp(12.34f, -HUGE_VALF, 10);
-  CheckNotWithinUlp(12.34f, std::nanf(""), 10);
-  CheckNotWithinUlp(12.34f, -12.34f, 10);
-}
-
-std::vector<Float16> ConvertToFloat16Vector(const std::vector<float>& 
float_values) {
-  std::vector<Float16> float16_vector;
-  float16_vector.reserve(float_values.size());
-  for (auto& value : float_values) {
-    float16_vector.emplace_back(value);
-  }
-  return float16_vector;
-}
-
-TEST(TestWithinUlp, Float16) {
-  for (Float16 f : ConvertToFloat16Vector({0.0f, 1e-8f, 1.0f, 123.456f})) {
-    CheckWithinUlp(f, f, 0);
-    CheckWithinUlp(f, f, 1);
-    CheckWithinUlp(f, f, 42);
-  }
-  CheckWithinUlp(Float16(-0.0f), Float16(0.0f), 1);
-  CheckWithinUlp(Float16(1.0f), Float16(1.00097656f), 1);
-  CheckWithinUlp(Float16(1.0f), Float16(1.01074219f), 11);
-  CheckNotWithinUlp(Float16(1.0f), Float16(1.00097656f), 0);
-  CheckNotWithinUlp(Float16(1.0f), Float16(1.01074219f), 10);
-  // left and right have a different exponent but are still very close
-  CheckWithinUlp(Float16(1.0f), Float16(0.999511719f), 1);
-  CheckWithinUlp(Float16(1.0f), Float16(0.994628906f), 11);
-  CheckNotWithinUlp(Float16(1.0f), Float16(0.994628906f), 10);
-  CheckWithinUlp(Float16(1.00097656), Float16(0.999511719f), 2);
-  CheckNotWithinUlp(Float16(1.00097656), Float16(0.999511719f), 1);
-  CheckWithinUlp(Float16(1.01074219f), Float16(0.994628906f), 22);
-  CheckNotWithinUlp(Float16(1.01074219f), Float16(0.994628906f), 21);
-
-  CheckWithinUlp(Float16(123.456f), Float16(124.143501f), 11);
-  // The assertion below does not work because ldexp(Float16(124.143501f), 10)
-  // results in inf in Float16.
-  // CheckNotWithinUlp(Float16(123.456f), Float16(124.143501f), 10);
-
-  CheckWithinUlp(std::numeric_limits<Float16>::infinity(),
-                 std::numeric_limits<Float16>::infinity(), 10);
-  CheckWithinUlp(-std::numeric_limits<Float16>::infinity(),
-                 -std::numeric_limits<Float16>::infinity(), 10);
-  CheckWithinUlp(std::numeric_limits<Float16>::quiet_NaN(),
-                 std::numeric_limits<Float16>::quiet_NaN(), 10);
-  CheckNotWithinUlp(std::numeric_limits<Float16>::infinity(),
-                    -std::numeric_limits<Float16>::infinity(), 10);
-  CheckNotWithinUlp(Float16(12.34f), 
-std::numeric_limits<Float16>::infinity(), 10);
-  CheckNotWithinUlp(Float16(12.34f), 
std::numeric_limits<Float16>::quiet_NaN(), 10);
-  CheckNotWithinUlp(Float16(12.34f), Float16(-12.34f), 10);
-}
-
 TEST(AssertTestWithinUlp, Basics) {
   AssertWithinUlp(123.4567, 123.45670000000015, 11);
   AssertWithinUlp(123.456f, 123.456085f, 11);
   AssertWithinUlp(Float16(123.456f), Float16(124.143501f), 11);
+  AssertWithinUlp(std::numeric_limits<float>::quiet_NaN(),
+                  std::numeric_limits<float>::quiet_NaN(), 2);
+  AssertWithinUlp(std::numeric_limits<double>::quiet_NaN(),
+                  std::numeric_limits<double>::quiet_NaN(), 2);
+  AssertWithinUlp(std::numeric_limits<Float16>::quiet_NaN(),
+                  std::numeric_limits<Float16>::quiet_NaN(), 2);
 #ifndef _WIN32
   // GH-47442
   EXPECT_FATAL_FAILURE(AssertWithinUlp(123.4567, 123.45670000000015, 10),
diff --git a/cpp/src/arrow/testing/math.cc b/cpp/src/arrow/testing/math.cc
index 79f7ec3033..df65544f06 100644
--- a/cpp/src/arrow/testing/math.cc
+++ b/cpp/src/arrow/testing/math.cc
@@ -25,90 +25,32 @@
 #include <gtest/gtest.h>
 
 #include "arrow/util/float16.h"
-#include "arrow/util/logging_internal.h"
-#include "arrow/util/ubsan.h"
+#include "arrow/util/ulp_distance_internal.h"
 
 namespace arrow {
 namespace {
 
 template <typename Float>
-struct FloatToUInt;
-
-template <>
-struct FloatToUInt<double> {
-  using Type = uint64_t;
-};
-
-template <>
-struct FloatToUInt<float> {
-  using Type = uint32_t;
-};
-
-template <>
-struct FloatToUInt<util::Float16> {
-  using Type = uint16_t;
-};
-
-template <typename Float>
-struct UlpDistanceUtil {
- public:
-  using UIntType = typename FloatToUInt<Float>::Type;
-  static constexpr UIntType kNumberOfBits = sizeof(Float) * 8;
-  static constexpr UIntType kSignMask = static_cast<UIntType>(1) << 
(kNumberOfBits - 1);
-
-  // This implementation is inspired by:
-  // 
https://randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition/
-  static UIntType UlpDistance(Float left, Float right) {
-    auto unsigned_left = util::SafeCopy<UIntType>(left);
-    auto unsigned_right = util::SafeCopy<UIntType>(right);
-    auto biased_left = ConvertSignAndMagnitudeToBiased(unsigned_left);
-    auto biased_right = ConvertSignAndMagnitudeToBiased(unsigned_right);
-    if (biased_left > biased_right) {
-      std::swap(biased_left, biased_right);
-    }
-    return biased_right - biased_left;
-  }
-
- private:
-  // Source reference (GoogleTest):
-  // 
https://github.com/google/googletest/blob/1b96fa13f549387b7549cc89e1a785cf143a1a50/googletest/include/gtest/internal/gtest-internal.h#L345-L368
-  static UIntType ConvertSignAndMagnitudeToBiased(UIntType value) {
-    if (value & kSignMask) {
-      return ~value + 1;
-    } else {
-      return value | kSignMask;
-    }
-  }
-};
-
-template <typename Float>
-bool WithinUlpGeneric(Float left, Float right, int n_ulps) {
+bool WithinUlpGeneric(Float left, Float right, int32_t n_ulps) {
   if constexpr (std::is_same_v<Float, util::Float16>) {
     if (left.is_nan() || right.is_nan()) {
       return left.is_nan() == right.is_nan();
-    } else if (left.is_infinity() || right.is_infinity()) {
-      return left == right;
     }
   } else {
     if (std::isnan(left) || std::isnan(right)) {
       return std::isnan(left) == std::isnan(right);
     }
-    if (!std::isfinite(left) || !std::isfinite(right)) {
-      return left == right;
-    }
   }
 
   if (n_ulps == 0) {
     return left == right;
   }
 
-  DCHECK_GE(n_ulps, 1);
-  return UlpDistanceUtil<Float>::UlpDistance(left, right) <=
-         static_cast<uint64_t>(n_ulps);
+  return internal::WithinUlp(left, right, n_ulps);
 }
 
 template <typename Float>
-void AssertWithinUlpGeneric(Float left, Float right, int n_ulps) {
+void AssertWithinUlpGeneric(Float left, Float right, int32_t n_ulps) {
   if (!WithinUlpGeneric(left, right, n_ulps)) {
     FAIL() << left << " and " << right << " are not within " << n_ulps << " 
ulps";
   }
@@ -116,27 +58,15 @@ void AssertWithinUlpGeneric(Float left, Float right, int 
n_ulps) {
 
 }  // namespace
 
-bool WithinUlp(util::Float16 left, util::Float16 right, int n_ulps) {
-  return WithinUlpGeneric(left, right, n_ulps);
-}
-
-bool WithinUlp(float left, float right, int n_ulps) {
-  return WithinUlpGeneric(left, right, n_ulps);
-}
-
-bool WithinUlp(double left, double right, int n_ulps) {
-  return WithinUlpGeneric(left, right, n_ulps);
-}
-
-void AssertWithinUlp(util::Float16 left, util::Float16 right, int n_ulps) {
+void AssertWithinUlp(util::Float16 left, util::Float16 right, int32_t n_ulps) {
   AssertWithinUlpGeneric(left, right, n_ulps);
 }
 
-void AssertWithinUlp(float left, float right, int n_ulps) {
+void AssertWithinUlp(float left, float right, int32_t n_ulps) {
   AssertWithinUlpGeneric(left, right, n_ulps);
 }
 
-void AssertWithinUlp(double left, double right, int n_ulps) {
+void AssertWithinUlp(double left, double right, int32_t n_ulps) {
   AssertWithinUlpGeneric(left, right, n_ulps);
 }
 
diff --git a/cpp/src/arrow/testing/math.h b/cpp/src/arrow/testing/math.h
index 1e829e0d61..ffe9628a02 100644
--- a/cpp/src/arrow/testing/math.h
+++ b/cpp/src/arrow/testing/math.h
@@ -23,17 +23,10 @@
 namespace arrow {
 
 ARROW_TESTING_EXPORT
-bool WithinUlp(util::Float16 left, util::Float16 right, int n_ulps);
+void AssertWithinUlp(util::Float16 left, util::Float16 right, int32_t n_ulps);
 ARROW_TESTING_EXPORT
-bool WithinUlp(float left, float right, int n_ulps);
+void AssertWithinUlp(float left, float right, int32_t n_ulps);
 ARROW_TESTING_EXPORT
-bool WithinUlp(double left, double right, int n_ulps);
-
-ARROW_TESTING_EXPORT
-void AssertWithinUlp(util::Float16 left, util::Float16 right, int n_ulps);
-ARROW_TESTING_EXPORT
-void AssertWithinUlp(float left, float right, int n_ulps);
-ARROW_TESTING_EXPORT
-void AssertWithinUlp(double left, double right, int n_ulps);
+void AssertWithinUlp(double left, double right, int32_t n_ulps);
 
 }  // namespace arrow
diff --git a/cpp/src/arrow/util/CMakeLists.txt 
b/cpp/src/arrow/util/CMakeLists.txt
index 8b05b4b6e6..28ea215bd2 100644
--- a/cpp/src/arrow/util/CMakeLists.txt
+++ b/cpp/src/arrow/util/CMakeLists.txt
@@ -81,6 +81,7 @@ add_arrow_test(utility-test
                tracing_test.cc
                trie_test.cc
                uri_test.cc
+               ulp_distance_test.cc
                utf8_util_test.cc
                value_parsing_test.cc
                EXTRA_LINK_LIBS
diff --git a/cpp/src/arrow/util/meson.build b/cpp/src/arrow/util/meson.build
index e21537b0d4..c39e09c2d0 100644
--- a/cpp/src/arrow/util/meson.build
+++ b/cpp/src/arrow/util/meson.build
@@ -216,6 +216,7 @@ utility_test_srcs = [
     'time_test.cc',
     'tracing_test.cc',
     'trie_test.cc',
+    'ulp_distance_test.cc',
     'uri_test.cc',
     'utf8_util_test.cc',
     'value_parsing_test.cc',
diff --git a/cpp/src/arrow/testing/math.cc b/cpp/src/arrow/util/ulp_distance.cc
similarity index 64%
copy from cpp/src/arrow/testing/math.cc
copy to cpp/src/arrow/util/ulp_distance.cc
index 79f7ec3033..cbc6e92090 100644
--- a/cpp/src/arrow/testing/math.cc
+++ b/cpp/src/arrow/util/ulp_distance.cc
@@ -15,20 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
-#include "arrow/testing/math.h"
+#include "arrow/util/ulp_distance_internal.h"
 
 #include <algorithm>
+#include <cinttypes>
 #include <cmath>
-#include <limits>
 #include <type_traits>
 
-#include <gtest/gtest.h>
-
 #include "arrow/util/float16.h"
 #include "arrow/util/logging_internal.h"
 #include "arrow/util/ubsan.h"
 
-namespace arrow {
+namespace arrow::internal {
 namespace {
 
 template <typename Float>
@@ -52,7 +50,7 @@ struct FloatToUInt<util::Float16> {
 template <typename Float>
 struct UlpDistanceUtil {
  public:
-  using UIntType = typename FloatToUInt<Float>::Type;
+  using UIntType = FloatToUInt<Float>::Type;
   static constexpr UIntType kNumberOfBits = sizeof(Float) * 8;
   static constexpr UIntType kSignMask = static_cast<UIntType>(1) << 
(kNumberOfBits - 1);
 
@@ -66,6 +64,8 @@ struct UlpDistanceUtil {
     if (biased_left > biased_right) {
       std::swap(biased_left, biased_right);
     }
+
+    // Handling of NaN should be determined by the comparison policy.
     return biased_right - biased_left;
   }
 
@@ -82,62 +82,24 @@ struct UlpDistanceUtil {
 };
 
 template <typename Float>
-bool WithinUlpGeneric(Float left, Float right, int n_ulps) {
-  if constexpr (std::is_same_v<Float, util::Float16>) {
-    if (left.is_nan() || right.is_nan()) {
-      return left.is_nan() == right.is_nan();
-    } else if (left.is_infinity() || right.is_infinity()) {
-      return left == right;
-    }
-  } else {
-    if (std::isnan(left) || std::isnan(right)) {
-      return std::isnan(left) == std::isnan(right);
-    }
-    if (!std::isfinite(left) || !std::isfinite(right)) {
-      return left == right;
-    }
-  }
-
-  if (n_ulps == 0) {
-    return left == right;
-  }
-
-  DCHECK_GE(n_ulps, 1);
+bool WithinUlpGeneric(Float left, Float right, int32_t n_ulps) {
+  DCHECK_GE(n_ulps, 0);
   return UlpDistanceUtil<Float>::UlpDistance(left, right) <=
          static_cast<uint64_t>(n_ulps);
 }
 
-template <typename Float>
-void AssertWithinUlpGeneric(Float left, Float right, int n_ulps) {
-  if (!WithinUlpGeneric(left, right, n_ulps)) {
-    FAIL() << left << " and " << right << " are not within " << n_ulps << " 
ulps";
-  }
-}
-
 }  // namespace
 
-bool WithinUlp(util::Float16 left, util::Float16 right, int n_ulps) {
+bool WithinUlp(util::Float16 left, util::Float16 right, int32_t n_ulps) {
   return WithinUlpGeneric(left, right, n_ulps);
 }
 
-bool WithinUlp(float left, float right, int n_ulps) {
+bool WithinUlp(float left, float right, int32_t n_ulps) {
   return WithinUlpGeneric(left, right, n_ulps);
 }
 
-bool WithinUlp(double left, double right, int n_ulps) {
+bool WithinUlp(double left, double right, int32_t n_ulps) {
   return WithinUlpGeneric(left, right, n_ulps);
 }
 
-void AssertWithinUlp(util::Float16 left, util::Float16 right, int n_ulps) {
-  AssertWithinUlpGeneric(left, right, n_ulps);
-}
-
-void AssertWithinUlp(float left, float right, int n_ulps) {
-  AssertWithinUlpGeneric(left, right, n_ulps);
-}
-
-void AssertWithinUlp(double left, double right, int n_ulps) {
-  AssertWithinUlpGeneric(left, right, n_ulps);
-}
-
-}  // namespace arrow
+}  // namespace arrow::internal
diff --git a/cpp/src/arrow/testing/math.h 
b/cpp/src/arrow/util/ulp_distance_internal.h
similarity index 59%
copy from cpp/src/arrow/testing/math.h
copy to cpp/src/arrow/util/ulp_distance_internal.h
index 1e829e0d61..c6661d2198 100644
--- a/cpp/src/arrow/testing/math.h
+++ b/cpp/src/arrow/util/ulp_distance_internal.h
@@ -17,23 +17,18 @@
 
 #pragma once
 
-#include "arrow/testing/visibility.h"
 #include "arrow/type_fwd.h"
+#include "arrow/util/visibility.h"
 
-namespace arrow {
+namespace arrow::internal {
 
-ARROW_TESTING_EXPORT
-bool WithinUlp(util::Float16 left, util::Float16 right, int n_ulps);
-ARROW_TESTING_EXPORT
-bool WithinUlp(float left, float right, int n_ulps);
-ARROW_TESTING_EXPORT
-bool WithinUlp(double left, double right, int n_ulps);
+ARROW_EXPORT
+bool WithinUlp(util::Float16 left, util::Float16 right, int32_t n_ulps);
 
-ARROW_TESTING_EXPORT
-void AssertWithinUlp(util::Float16 left, util::Float16 right, int n_ulps);
-ARROW_TESTING_EXPORT
-void AssertWithinUlp(float left, float right, int n_ulps);
-ARROW_TESTING_EXPORT
-void AssertWithinUlp(double left, double right, int n_ulps);
+ARROW_EXPORT
+bool WithinUlp(float left, float right, int32_t n_ulps);
 
-}  // namespace arrow
+ARROW_EXPORT
+bool WithinUlp(double left, double right, int32_t n_ulps);
+
+}  // namespace arrow::internal
diff --git a/cpp/src/arrow/testing/gtest_util_test.cc 
b/cpp/src/arrow/util/ulp_distance_test.cc
similarity index 52%
copy from cpp/src/arrow/testing/gtest_util_test.cc
copy to cpp/src/arrow/util/ulp_distance_test.cc
index 2a28df79dd..e2c84e25f3 100644
--- a/cpp/src/arrow/testing/gtest_util_test.cc
+++ b/cpp/src/arrow/util/ulp_distance_test.cc
@@ -15,173 +15,24 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include "arrow/util/ulp_distance_internal.h"
+
+#include <cinttypes>
 #include <cmath>
-#include <memory>
 #include <type_traits>
 #include <vector>
 
-#include <gtest/gtest-spi.h>
-#include <gtest/gtest.h>
+#include "gtest/gtest.h"
 
-#include "arrow/array.h"
-#include "arrow/array/builder_decimal.h"
-#include "arrow/datum.h"
-#include "arrow/record_batch.h"
-#include "arrow/table.h"
-#include "arrow/tensor.h"
 #include "arrow/testing/gtest_util.h"
-#include "arrow/testing/math.h"
-#include "arrow/testing/random.h"
-#include "arrow/type.h"
-#include "arrow/type_traits.h"
-#include "arrow/util/checked_cast.h"
 #include "arrow/util/float16.h"
 
-namespace arrow::util {
-
-// Test basic cases for contains NaN.
-class TestAssertContainsNaN : public ::testing::Test {};
-
-TEST_F(TestAssertContainsNaN, BatchesEqual) {
-  auto schema = ::arrow::schema({
-      {field("a", float32())},
-      {field("b", float64())},
-  });
-
-  auto expected = RecordBatchFromJSON(schema,
-                                      R"([{"a": 3,    "b": 5},
-                                       {"a": 1,    "b": 3},
-                                       {"a": 3,    "b": 4},
-                                       {"a": NaN,  "b": 6},
-                                       {"a": 2,    "b": 5},
-                                       {"a": 1,    "b": NaN},
-                                       {"a": 1,    "b": 3}
-                                       ])");
-  auto actual = RecordBatchFromJSON(schema,
-                                    R"([{"a": 3,    "b": 5},
-                                       {"a": 1,    "b": 3},
-                                       {"a": 3,    "b": 4},
-                                       {"a": NaN,  "b": 6},
-                                       {"a": 2,    "b": 5},
-                                       {"a": 1,    "b": NaN},
-                                       {"a": 1,    "b": 3}
-                                       ])");
-  ASSERT_BATCHES_EQUAL(*expected, *actual);
-  AssertBatchesApproxEqual(*expected, *actual);
-}
-
-TEST_F(TestAssertContainsNaN, TableEqual) {
-  auto schema = ::arrow::schema({
-      {field("a", float32())},
-      {field("b", float64())},
-  });
-
-  auto expected = TableFromJSON(schema, {R"([{"a": null, "b": 5},
-                                     {"a": NaN,    "b": 3},
-                                     {"a": 3,    "b": null}
-                                    ])",
-                                         R"([{"a": null, "b": null},
-                                     {"a": 2,    "b": NaN},
-                                     {"a": 1,    "b": 5},
-                                     {"a": 3,    "b": 5}
-                                    ])"});
-  auto actual = TableFromJSON(schema, {R"([{"a": null, "b": 5},
-                                     {"a": NaN,    "b": 3},
-                                     {"a": 3,    "b": null}
-                                    ])",
-                                       R"([{"a": null, "b": null},
-                                     {"a": 2,    "b": NaN},
-                                     {"a": 1,    "b": 5},
-                                     {"a": 3,    "b": 5}
-                                    ])"});
-  ASSERT_TABLES_EQUAL(*expected, *actual);
-}
-
-TEST_F(TestAssertContainsNaN, ArrayEqual) {
-  auto expected = ArrayFromJSON(float64(), "[0, 1, 2, NaN]");
-  auto actual = ArrayFromJSON(float64(), "[0, 1, 2, NaN]");
-  AssertArraysEqual(*expected, *actual);
-}
-
-TEST_F(TestAssertContainsNaN, ChunkedEqual) {
-  auto expected = ChunkedArrayFromJSON(float64(), {
-                                                      "[null, 1]",
-                                                      "[3, NaN, 2]",
-                                                      "[NaN]",
-                                                  });
-
-  auto actual = ChunkedArrayFromJSON(float64(), {
-                                                    "[null, 1]",
-                                                    "[3, NaN, 2]",
-                                                    "[NaN]",
-                                                });
-  AssertChunkedEqual(*expected, *actual);
-}
-
-TEST_F(TestAssertContainsNaN, DatumEqual) {
-  // scalar
-  auto expected_scalar = ScalarFromJSON(float64(), "NaN");
-  auto actual_scalar = ScalarFromJSON(float64(), "NaN");
-  AssertDatumsEqual(expected_scalar, actual_scalar);
-
-  // array
-  auto expected_array = ArrayFromJSON(float64(), "[3, NaN, 2, 1, 5]");
-  auto actual_array = ArrayFromJSON(float64(), "[3, NaN, 2, 1, 5]");
-  AssertDatumsEqual(expected_array, actual_array);
+namespace arrow::internal {
 
-  // chunked array
-  auto expected_chunked = ChunkedArrayFromJSON(float64(), {
-                                                              "[null, 1]",
-                                                              "[3, NaN, 2]",
-                                                              "[NaN]",
-                                                          });
-
-  auto actual_chunked = ChunkedArrayFromJSON(float64(), {
-                                                            "[null, 1]",
-                                                            "[3, NaN, 2]",
-                                                            "[NaN]",
-                                                        });
-  AssertDatumsEqual(expected_chunked, actual_chunked);
-}
-
-class TestTensorFromJSON : public ::testing::Test {};
-
-TEST_F(TestTensorFromJSON, FromJSONAndArray) {
-  std::vector<int64_t> shape = {9, 2};
-  const int64_t i64_size = sizeof(int64_t);
-  std::vector<int64_t> f_strides = {i64_size, i64_size * shape[0]};
-  std::vector<int64_t> f_values = {1,  2,  3,  4,  5,  6,  7,  8,  9,
-                                   10, 20, 30, 40, 50, 60, 70, 80, 90};
-  auto data = Buffer::Wrap(f_values);
-
-  std::shared_ptr<Tensor> tensor_expected;
-  ASSERT_OK_AND_ASSIGN(tensor_expected, Tensor::Make(int64(), data, shape, 
f_strides));
-
-  std::shared_ptr<Tensor> result = TensorFromJSON(
-      int64(), "[1, 2,  3,  4,  5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 
90]",
-      shape, f_strides);
-
-  EXPECT_TRUE(tensor_expected->Equals(*result));
-}
-
-TEST_F(TestTensorFromJSON, FromJSON) {
-  std::vector<int64_t> shape = {9, 2};
-  std::vector<int64_t> values = {1,  2,  3,  4,  5,  6,  7,  8,  9,
-                                 10, 20, 30, 40, 50, 60, 70, 80, 90};
-  auto data = Buffer::Wrap(values);
-
-  std::shared_ptr<Tensor> tensor_expected;
-  ASSERT_OK_AND_ASSIGN(tensor_expected, Tensor::Make(int64(), data, shape));
-
-  std::shared_ptr<Tensor> result = TensorFromJSON(
-      int64(), "[1, 2,  3,  4,  5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 
90]",
-      "[9, 2]");
-
-  EXPECT_TRUE(tensor_expected->Equals(*result));
-}
+using util::Float16;
 
 template <typename Float>
-void CheckWithinUlpSingle(Float x, Float y, int n_ulp) {
+void CheckWithinUlpSingle(Float x, Float y, int32_t n_ulp) {
   ARROW_SCOPED_TRACE("x = ", x, ", y = ", y, ", n_ulp = ", n_ulp);
   ASSERT_TRUE(WithinUlp(x, y, n_ulp));
 }
@@ -357,18 +208,4 @@ TEST(TestWithinUlp, Float16) {
   CheckNotWithinUlp(Float16(12.34f), Float16(-12.34f), 10);
 }
 
-TEST(AssertTestWithinUlp, Basics) {
-  AssertWithinUlp(123.4567, 123.45670000000015, 11);
-  AssertWithinUlp(123.456f, 123.456085f, 11);
-  AssertWithinUlp(Float16(123.456f), Float16(124.143501f), 11);
-#ifndef _WIN32
-  // GH-47442
-  EXPECT_FATAL_FAILURE(AssertWithinUlp(123.4567, 123.45670000000015, 10),
-                       "not within 10 ulps");
-  EXPECT_FATAL_FAILURE(AssertWithinUlp(123.456f, 123.456085f, 10), "not within 
10 ulps");
-  EXPECT_FATAL_FAILURE(AssertWithinUlp(Float16(123.456f), 
Float16(124.143501f), 10),
-                       "not within 10 ulps");
-#endif
-}
-
-}  // namespace arrow::util
+}  // namespace arrow::internal

Reply via email to