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

apitrou 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 0d8c8d9581 GH-44915: [C++] Add WithinUlp testing functions (#44906)
0d8c8d9581 is described below

commit 0d8c8d9581cc5c2a21881bb8665a634fef07433a
Author: Antoine Pitrou <[email protected]>
AuthorDate: Mon Dec 9 12:08:29 2024 +0100

    GH-44915: [C++] Add WithinUlp testing functions (#44906)
    
    ### Rationale for this change
    
    When testing math-related functions, we might want to check that some 
results are very close to an expected value, but not necessarily exactly equal.
    
    ### What changes are included in this PR?
    
    Add functions that test whether two floating-point values are within N ulps.
    
    ("ulp" stands for "unit in the last place": 
https://en.wikipedia.org/wiki/Unit_in_the_last_place)
    
    ### Are these changes tested?
    
    Yes.
    
    ### Are there any user-facing changes?
    
    Potentially more useful error messages.
    
    * GitHub Issue: #44915
    
    Authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/CMakeLists.txt             |   1 +
 cpp/src/arrow/testing/gtest_util_test.cc | 110 +++++++++++++++++++++++++++++++
 cpp/src/arrow/testing/math.cc            |  88 +++++++++++++++++++++++++
 cpp/src/arrow/testing/math.h             |  34 ++++++++++
 cpp/src/arrow/util/string_builder.h      |   8 ++-
 5 files changed, 240 insertions(+), 1 deletion(-)

diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt
index 4e40056839..f1f3b1c30b 100644
--- a/cpp/src/arrow/CMakeLists.txt
+++ b/cpp/src/arrow/CMakeLists.txt
@@ -674,6 +674,7 @@ set(ARROW_TESTING_SRCS
     testing/fixed_width_test_util.cc
     testing/generator.cc
     testing/gtest_util.cc
+    testing/math.cc
     testing/process.cc
     testing/random.cc
     testing/util.cc)
diff --git a/cpp/src/arrow/testing/gtest_util_test.cc 
b/cpp/src/arrow/testing/gtest_util_test.cc
index 9b4514197d..daf071c2b3 100644
--- a/cpp/src/arrow/testing/gtest_util_test.cc
+++ b/cpp/src/arrow/testing/gtest_util_test.cc
@@ -15,6 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <cmath>
+
+#include <gtest/gtest-spi.h>
 #include <gtest/gtest.h>
 
 #include "arrow/array.h"
@@ -23,6 +26,7 @@
 #include "arrow/record_batch.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"
@@ -171,4 +175,110 @@ 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 = std::ldexp(x, exp);
+    Float 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 = std::ldexp(x, exp);
+    Float 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, 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.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(123.4567, 123.45670000000015, 11);
+  CheckNotWithinUlp(123.4567, 123.45670000000015, 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, 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.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(123.456f, 123.456085f, 11);
+  CheckNotWithinUlp(123.456f, 123.456085f, 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);
+}
+
+TEST(AssertTestWithinUlp, Basics) {
+  AssertWithinUlp(123.4567, 123.45670000000015, 11);
+  AssertWithinUlp(123.456f, 123.456085f, 11);
+  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");
+}
+
 }  // namespace arrow
diff --git a/cpp/src/arrow/testing/math.cc b/cpp/src/arrow/testing/math.cc
new file mode 100644
index 0000000000..c3246b1221
--- /dev/null
+++ b/cpp/src/arrow/testing/math.cc
@@ -0,0 +1,88 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "arrow/testing/math.h"
+
+#include <cmath>
+#include <limits>
+
+#include <gtest/gtest.h>
+
+#include "arrow/util/logging.h"
+
+namespace arrow {
+namespace {
+
+template <typename Float>
+bool WithinUlpOneWay(Float left, Float right, int n_ulp) {
+  // The delta between 1.0 and the FP value immediately before it.
+  // We're using this value because `frexp` returns a mantissa between 0.5 and 
1.0.
+  static const Float kOneUlp = Float(1.0) - std::nextafter(Float(1.0), 
Float(0.0));
+
+  DCHECK_GE(n_ulp, 1);
+
+  if (left == 0) {
+    return left == right;
+  }
+  if (left < 0) {
+    left = -left;
+    right = -right;
+  }
+
+  int left_exp;
+  Float left_mant = std::frexp(left, &left_exp);
+  Float delta = static_cast<Float>(n_ulp) * kOneUlp;
+  Float lower_bound = std::ldexp(left_mant - delta, left_exp);
+  Float upper_bound = std::ldexp(left_mant + delta, left_exp);
+  return right >= lower_bound && right <= upper_bound;
+}
+
+template <typename Float>
+bool WithinUlpGeneric(Float left, Float right, int n_ulp) {
+  if (!std::isfinite(left) || !std::isfinite(right)) {
+    return left == right;
+  }
+  return (std::abs(left) <= std::abs(right)) ? WithinUlpOneWay(left, right, 
n_ulp)
+                                             : WithinUlpOneWay(right, left, 
n_ulp);
+}
+
+template <typename Float>
+void AssertWithinUlpGeneric(Float left, Float right, int n_ulp) {
+  if (!WithinUlpGeneric(left, right, n_ulp)) {
+    FAIL() << left << " and " << right << " are not within " << n_ulp << " 
ulps";
+  }
+}
+
+}  // namespace
+
+bool WithinUlp(float left, float right, int n_ulp) {
+  return WithinUlpGeneric(left, right, n_ulp);
+}
+
+bool WithinUlp(double left, double right, int n_ulp) {
+  return WithinUlpGeneric(left, right, n_ulp);
+}
+
+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
diff --git a/cpp/src/arrow/testing/math.h b/cpp/src/arrow/testing/math.h
new file mode 100644
index 0000000000..19001ac177
--- /dev/null
+++ b/cpp/src/arrow/testing/math.h
@@ -0,0 +1,34 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include "arrow/testing/visibility.h"
+
+namespace arrow {
+
+ARROW_TESTING_EXPORT
+bool WithinUlp(float left, float right, int n_ulp);
+ARROW_TESTING_EXPORT
+bool WithinUlp(double left, double right, int n_ulp);
+
+ARROW_TESTING_EXPORT
+void AssertWithinUlp(float left, float right, int n_ulps);
+ARROW_TESTING_EXPORT
+void AssertWithinUlp(double left, double right, int n_ulps);
+
+}  // namespace arrow
diff --git a/cpp/src/arrow/util/string_builder.h 
b/cpp/src/arrow/util/string_builder.h
index 7c05ccd51f..448fb57d7a 100644
--- a/cpp/src/arrow/util/string_builder.h
+++ b/cpp/src/arrow/util/string_builder.h
@@ -20,6 +20,7 @@
 #include <memory>
 #include <ostream>
 #include <string>
+#include <type_traits>
 #include <utility>
 
 #include "arrow/util/visibility.h"
@@ -46,7 +47,12 @@ class ARROW_EXPORT StringStreamWrapper {
 
 template <typename Head>
 void StringBuilderRecursive(std::ostream& stream, Head&& head) {
-  stream << head;
+  if constexpr (std::is_floating_point_v<std::decay_t<Head>>) {
+    // Avoid losing precision when printing floating point numbers
+    stream << std::to_string(head);
+  } else {
+    stream << head;
+  }
 }
 
 template <typename Head, typename... Tail>

Reply via email to