You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2021/04/22 17:38:12 UTC
[commons-numbers] branch master updated: NUMBERS-77: Equivalence of
double values.
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 <ma...@apache.org>
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 <ah...@apache.org>
Co-authored-by: Gilles Sadowski <gi...@gmail.com>
---
.../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;
+ }
+}