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 <mattjuntu...@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 <aherb...@apache.org> Co-authored-by: Gilles Sadowski <gillese...@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; + } +}