This is an automated email from the ASF dual-hosted git repository.
erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-numbers.git
The following commit(s) were added to refs/heads/master by this push:
new dbd2a47 NUMBERS-77: Equivalence of double values.
dbd2a47 is described below
commit dbd2a473e4949d895054190f61cb950da1d6b36d
Author: Matt Juntunen <[email protected]>
AuthorDate: Wed Apr 21 17:02:50 2021 -0400
NUMBERS-77: Equivalence of double values.
Functionality derived from class "DoublePrecisionContext" originally
defined in Commons Geometry.
Closes #89.
Co-authored-by: Alex Herbert <[email protected]>
Co-authored-by: Gilles Sadowski <[email protected]>
---
.../org/apache/commons/numbers/core/Precision.java | 133 +++++++++++++++
.../numbers/core/EpsilonDoubleEquivalenceTest.java | 181 +++++++++++++++++++++
2 files changed, 314 insertions(+)
diff --git
a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Precision.java
b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Precision.java
index 686bd31..6d32631 100644
---
a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Precision.java
+++
b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Precision.java
@@ -500,4 +500,137 @@ public final class Precision {
double delta) {
return x + delta - x;
}
+
+ /**
+ * Creates a {@link DoubleEquivalence} instance that uses the given epsilon
+ * value for determining equality.
+ *
+ * @param eps Value to use for determining equality.
+ * @return a new instance.
+ */
+ public static DoubleEquivalence doubleEquivalenceOfEpsilon(final double
eps) {
+ if (!Double.isFinite(eps) ||
+ eps < 0d) {
+ throw new IllegalArgumentException("Invalid epsilon value: " +
eps);
+ }
+
+ return new DoubleEquivalence() {
+ /** Epsilon value. */
+ private final double epsilon = eps;
+
+ /** {@inheritDoc} */
+ @Override
+ public int compare(double a,
+ double b) {
+ return Precision.compareTo(a, b, epsilon);
+ }
+ };
+ }
+
+ /**
+ * Interface containing comparison operations for doubles that allow values
+ * to be <em>considered</em> equal even if they are not exactly equal.
+ * It is intended for comparing outputs of a computation where floating
+ * point errors may have occurred.
+ */
+ public interface DoubleEquivalence {
+ /**
+ * Indicates whether given values are considered equal to each other.
+ *
+ * @param a Value.
+ * @param b Value.
+ * @return true if the given values are considered equal.
+ */
+ default boolean eq(double a, double b) {
+ return compare(a, b) == 0;
+ }
+
+ /**
+ * Indicates whether the given value is considered equal to zero.
+ * It is a shortcut for {@code eq(a, 0.0)}.
+ *
+ * @param a Value.
+ * @return true if the argument is considered equal to zero.
+ */
+ default boolean eqZero(double a) {
+ return eq(a, 0d);
+ }
+
+ /**
+ * Indicates whether the first argument is strictly smaller than the
second.
+ *
+ * @param a Value.
+ * @param b Value.
+ * @return true if {@code a < b}
+ */
+ default boolean lt(double a, double b) {
+ return compare(a, b) < 0;
+ }
+
+ /**
+ * Indicates whether the first argument is smaller or considered equal
to the second.
+ *
+ * @param a Value.
+ * @param b Value.
+ * @return true if {@code a <= b}
+ */
+ default boolean lte(double a, double b) {
+ return compare(a, b) <= 0;
+ }
+
+ /**
+ * Indicates whether the first argument is strictly greater than the
second.
+ *
+ * @param a Value.
+ * @param b Value.
+ * @return true if {@code a > b}
+ */
+ default boolean gt(double a, double b) {
+ return compare(a, b) > 0;
+ }
+
+ /**
+ * Indicates whether the first argument is greater than or considered
equal to the second.
+ *
+ * @param a Value.
+ * @param b Value.
+ * @return true if {@code a >= b}
+ */
+ default boolean gte(double a, double b) {
+ return compare(a, b) >= 0;
+ }
+
+ /**
+ * Returns the {@link Math#signum(double) sign} of the argument.
+ *
+ * @param a Value.
+ * @return the sign (or {@code a} if {@code eqZero(a)} is true or
+ * {@code a} is NaN).
+ */
+ default double signum(double a) {
+ return a == 0d ||
+ Double.isNaN(a) ?
+ a :
+ eqZero(a) ?
+ Math.copySign(0d, a) :
+ Math.copySign(1d, a);
+ }
+
+ /**
+ * Compares two values.
+ * The returned value is
+ * <ul>
+ * <li>{@code 0} if the arguments are considered equal,</li>
+ * <li>{@code -1} if {@code a < b},</li>
+ * <li>{@code +1} if {@code a > b} or if either value is NaN.</li>
+ * </ul>
+ *
+ * @param a Value.
+ * @param b Value.
+ * @return {@code 0} if the values are considered equal, {@code -1}
+ * if the first is smaller than the second, {@code 1} is the first
+ * is larger than the second or either value is NaN.
+ */
+ int compare(double a, double b);
+ }
}
diff --git
a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/EpsilonDoubleEquivalenceTest.java
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/EpsilonDoubleEquivalenceTest.java
new file mode 100644
index 0000000..623c95d
--- /dev/null
+++
b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/EpsilonDoubleEquivalenceTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+package org.apache.commons.numbers.core;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link Precision#DoubleEquivalence} instances created with
+ * {@link Precision#doubleEquivalenceOfEpsilon(double)}.
+ */
+class EpsilonDoubleEquivalenceTest {
+ @Test
+ void testInvalidEpsilonValues() {
+ // act/assert
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
Precision.doubleEquivalenceOfEpsilon(-1d));
+
+ String msg;
+
+ msg = Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
Precision.doubleEquivalenceOfEpsilon(Double.NaN)).getMessage();
+ Assertions.assertEquals("Invalid epsilon value: NaN", msg);
+
+ msg = Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
Precision.doubleEquivalenceOfEpsilon(Double.POSITIVE_INFINITY)).getMessage();
+ Assertions.assertEquals("Invalid epsilon value: Infinity", msg);
+
+ msg = Assertions.assertThrows(IllegalArgumentException.class,
+ () ->
Precision.doubleEquivalenceOfEpsilon(Double.NEGATIVE_INFINITY)).getMessage();
+ Assertions.assertEquals("Invalid epsilon value: -Infinity", msg);
+ }
+
+ @Test
+ void testSignum() {
+ // arrange
+ final double eps = 1e-2;
+
+ final Precision.DoubleEquivalence cmp =
Precision.doubleEquivalenceOfEpsilon(eps);
+
+ // act/assert
+ Assertions.assertEquals(Double.POSITIVE_INFINITY, 1 / cmp.signum(0.0),
0d);
+ Assertions.assertEquals(Double.NEGATIVE_INFINITY, 1 /
cmp.signum(-0.0), 0d);
+
+ Assertions.assertEquals(Double.POSITIVE_INFINITY, 1 / cmp.signum(eps),
0d);
+ Assertions.assertEquals(Double.NEGATIVE_INFINITY, 1 /
cmp.signum(-eps), 0d);
+
+ Assertions.assertEquals(1, cmp.signum(Math.nextUp(eps)), 0d);
+ Assertions.assertEquals(-1, cmp.signum(Math.nextDown(-eps)), 0d);
+
+ Assertions.assertTrue(Double.isNaN(cmp.signum(Double.NaN)));
+ Assertions.assertEquals(1, cmp.signum(Double.POSITIVE_INFINITY), 0d);
+ Assertions.assertEquals(-1, cmp.signum(Double.NEGATIVE_INFINITY), 0d);
+ }
+
+ @Test
+ void testCompare_compareToZero() {
+ // arrange
+ final double eps = 1e-2;
+
+ final Precision.DoubleEquivalence cmp =
Precision.doubleEquivalenceOfEpsilon(eps);
+
+ // act/assert
+ Assertions.assertEquals(0, cmp.compare(0.0, 0.0));
+ Assertions.assertEquals(0, cmp.compare(+0.0, -0.0));
+ Assertions.assertEquals(0, cmp.compare(eps, -0.0));
+ Assertions.assertEquals(0, cmp.compare(+0.0, eps));
+
+ Assertions.assertEquals(0, cmp.compare(-eps, -0.0));
+ Assertions.assertEquals(0, cmp.compare(+0.0, -eps));
+
+ Assertions.assertEquals(-1, cmp.compare(0.0, 1.0));
+ Assertions.assertEquals(1, cmp.compare(1.0, 0.0));
+
+ Assertions.assertEquals(1, cmp.compare(0.0, -1.0));
+ Assertions.assertEquals(-1, cmp.compare(-1.0, 0.0));
+ }
+
+ @Test
+ void testCompare_compareNonZero() {
+ // arrange
+ final double eps = 1e-5;
+ final double small = 1e-3;
+ final double big = 1e100;
+
+ final Precision.DoubleEquivalence cmp =
Precision.doubleEquivalenceOfEpsilon(eps);
+
+ // act/assert
+ Assertions.assertEquals(0, cmp.compare(eps, 2 * eps));
+ Assertions.assertEquals(0, cmp.compare(-2 * eps, -eps));
+
+ Assertions.assertEquals(0, cmp.compare(small, small + (0.9 * eps)));
+ Assertions.assertEquals(0, cmp.compare(-small - (0.9 * eps), -small));
+
+ Assertions.assertEquals(0, cmp.compare(big, nextUp(big, 1)));
+ Assertions.assertEquals(0, cmp.compare(nextDown(-big, 1), -big));
+
+ Assertions.assertEquals(-1, cmp.compare(small, small + (1.1 * eps)));
+ Assertions.assertEquals(1, cmp.compare(-small, -small - (1.1 * eps)));
+
+ Assertions.assertEquals(-1, cmp.compare(big, nextUp(big, 2)));
+ Assertions.assertEquals(1, cmp.compare(-big, nextDown(-big, 2)));
+ }
+
+ @Test
+ void testCompare_NaN() {
+ // arrange
+ final Precision.DoubleEquivalence cmp =
Precision.doubleEquivalenceOfEpsilon(1e-6);
+
+ // act/assert
+ Assertions.assertEquals(1, cmp.compare(0, Double.NaN));
+ Assertions.assertEquals(1, cmp.compare(Double.NaN, 0));
+ Assertions.assertEquals(1, cmp.compare(Double.NaN, Double.NaN));
+
+ Assertions.assertEquals(1, cmp.compare(Double.POSITIVE_INFINITY,
Double.NaN));
+ Assertions.assertEquals(1, cmp.compare(Double.NaN,
Double.POSITIVE_INFINITY));
+
+ Assertions.assertEquals(1, cmp.compare(Double.NEGATIVE_INFINITY,
Double.NaN));
+ Assertions.assertEquals(1, cmp.compare(Double.NaN,
Double.NEGATIVE_INFINITY));
+ }
+
+ @Test
+ void testCompare_infinity() {
+ // arrange
+ final Precision.DoubleEquivalence cmp =
Precision.doubleEquivalenceOfEpsilon(1e-6);
+
+ // act/assert
+ Assertions.assertEquals(-1, cmp.compare(0, Double.POSITIVE_INFINITY));
+ Assertions.assertEquals(1, cmp.compare(Double.POSITIVE_INFINITY, 0));
+ Assertions.assertEquals(0, cmp.compare(Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY));
+
+ Assertions.assertEquals(1, cmp.compare(0, Double.NEGATIVE_INFINITY));
+ Assertions.assertEquals(-1, cmp.compare(Double.NEGATIVE_INFINITY, 0));
+ Assertions.assertEquals(0, cmp.compare(Double.NEGATIVE_INFINITY,
Double.NEGATIVE_INFINITY));
+ }
+
+ /**
+ * Increments the given double value {@code count} number of times
+ * using {@link Math#nextUp(double)}.
+ * @param n
+ * @param count
+ * @return
+ */
+ private static double nextUp(final double n, final int count) {
+ double result = n;
+ for (int i = 0; i < count; ++i) {
+ result = Math.nextUp(result);
+ }
+
+ return result;
+ }
+
+ /**
+ * Decrements the given double value {@code count} number of times
+ * using {@link Math#nextDown(double)}.
+ * @param n
+ * @param count
+ * @return
+ */
+ private static double nextDown(final double n, final int count) {
+ double result = n;
+ for (int i = 0; i < count; ++i) {
+ result = Math.nextDown(result);
+ }
+
+ return result;
+ }
+}