This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-numbers.git
commit 496740c6be6925c173c8860982ec9de238c40f2f Author: Alex Herbert <aherb...@apache.org> AuthorDate: Thu Jan 19 12:51:24 2023 +0000 NUMBERS-192: Add subtract method to Sum Updated user guide with subtract example and more usage details. --- .../java/org/apache/commons/numbers/core/Sum.java | 28 ++++++++++++++++++--- .../org/apache/commons/numbers/core/SumTest.java | 29 ++++++++++++++++++++++ .../apache/commons/numbers/core/UserGuideTest.java | 11 ++++++++ src/changes/changes.xml | 3 +++ src/site/apt/userguide/index.apt | 25 +++++++++++++++++-- 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java index cc5e7791..8953efd1 100644 --- a/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java +++ b/commons-numbers-core/src/main/java/org/apache/commons/numbers/core/Sum.java @@ -177,11 +177,31 @@ public final class Sum * @return this instance. */ public Sum add(final Sum other) { - // Pull both values first to ensure there are - // no issues when adding a sum to itself. - final double s = other.sum; - final double c = other.comp; + return add(other.sum, other.comp); + } + + /** + * Subtracts another sum from this sum. + * + * @param other Sum to subtract. + * @return this instance. + */ + public Sum subtract(final Sum other) { + return add(-other.sum, -other.comp); + } + /** + * Adds the sum and compensation to this sum. + * + * <p>This is a utility method to extract both values from a sum + * before addition to ensure there are no issues when adding a sum + * to itself. + * + * @param s Sum. + * @param c Compensation. + * @return this instance. + */ + private Sum add(double s, double c) { return add(s).add(c); } diff --git a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java index f358ba36..5e42a727 100644 --- a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java +++ b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/SumTest.java @@ -102,6 +102,35 @@ class SumTest { Assertions.assertEquals(exactSum(a, b, a, b), s.add(s).getAsDouble()); } + @Test + void testSubtract_sumInstance() { + // arrange + final double a = Math.PI; + final double b = Math.scalb(a, -53); + final double c = Math.scalb(a, -53); + final double d = Math.scalb(a, -27); + final double e = Math.scalb(a, -27); + final double f = Math.scalb(a, -50); + + // act/assert + // add(-sum) == subtract(sum) + Assertions.assertEquals( + Sum.of(a, b, c, d).add(Sum.create()).getAsDouble(), + Sum.of(a, b, c, d).subtract(Sum.create()).getAsDouble()); + Assertions.assertEquals( + Sum.of(a, b, c, d).add(Sum.of(-e, -f)).getAsDouble(), + Sum.of(a, b, c, d).subtract(Sum.of(e, f)).getAsDouble()); + Assertions.assertEquals( + Sum.of(a, b).add(Sum.of(-a, -c).add(Sum.of(-d, -e, -f))).getAsDouble(), + Sum.of(a, b).subtract(Sum.of(a, c)).subtract(Sum.of(d, e, f)).getAsDouble()); + Assertions.assertEquals( + Sum.of(a, b).add(Sum.of(-a, c).add(Sum.of(-d, e, -f))).getAsDouble(), + Sum.of(a, b).subtract(Sum.of(a, -c)).subtract(Sum.of(d, -e, f)).getAsDouble()); + + final Sum s = Sum.of(a, b); + Assertions.assertEquals(0, s.subtract(s).getAsDouble()); + } + @Test void testSumOfProducts_dimensionMismatch() { // act/assert diff --git a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/UserGuideTest.java b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/UserGuideTest.java index 537c1a92..10f4d6e2 100644 --- a/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/UserGuideTest.java +++ b/commons-numbers-core/src/test/java/org/apache/commons/numbers/core/UserGuideTest.java @@ -64,6 +64,17 @@ class UserGuideTest { Assertions.assertEquals(-1.0, x2); } + @Test + void testSum3() { + double x1 = 1e100 + 1 - 2 - 1e100; + Sum s1 = Sum.of(1e100, 1); + Sum s2 = Sum.of(2, 1e100); + double x2 = s1.subtract(s2).getAsDouble(); + Assertions.assertEquals(0.0, x1); + Assertions.assertEquals(-1.0, x2); + } + + @Test void testPrecision1() { // Default allows no numbers between diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 569fdc6b..914b4af3 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -56,6 +56,9 @@ If the output is not quite correct, check for invisible trailing spaces! <release version="1.2" date="TBD" description=" New features, updates and bug fixes. "> + <action dev="aherbert" type="add" issue="NUMBERS-192"> + "Sum": Add subtract(Sum) method. + </action> <action dev="aherbert" type="add" issue="NUMBERS-191"> "Stirling": Compute Stirling numbers of the first kind. </action> diff --git a/src/site/apt/userguide/index.apt b/src/site/apt/userguide/index.apt index f140f894..8cf82a4d 100644 --- a/src/site/apt/userguide/index.apt +++ b/src/site/apt/userguide/index.apt @@ -411,7 +411,7 @@ Sum.of(1, 2, Double.NaN).getAsDouble() // NaN Sum.of(1, 2, Double.NEGATIVE_INFINITY).getAsDouble() // -inf +------------------------------------------+ - The implementation provides an effective 106 bit floating point significand. However the + The implementation provides up to a 106 bit floating point significand. However the significand is split into two <<<double>>> values which may be separated by more than <<<2^53>>> by using the exponent of the second <<<double>>>. This provides protection against cancellation in situations that would not be handled by an @@ -422,14 +422,35 @@ double x1 = 1e100 + 1 - 2 - 1e100; double x2 = Sum.of(1e100, 1, -2, -1e100).getAsDouble(); // x1 == 0.0 // x2 == -1.0 ++------------------------------------------+ + + Note that the first part of the sum is maintained as the IEEE754 result. The <<<Sum>>> + is therefore not a full <<<double-double>>> implementation, which would maintain the sum + as the current total and a round-off term. This difference makes the <<<Sum>>> class faster + at the cost of some accuracy during addition of terms that cancel. + + If the terms to be subtracted are known then the summation can be split into the positive + and negative terms, summed in two parts and the result computed by a final subtraction of + the <<<Sum>>> of negative parts. + ++------------------------------------------+ +double x1 = 1e100 + 1 - 2 - 1e100; +Sum s1 = Sum.of(1e100, 1); +Sum s2 = Sum.of(2, 1e100); +double x2 = s1.subtract(s2).getAsDouble(); +// x1 == 0.0 +// x2 == -1.0 +------------------------------------------+ The class can be used to: + * Provide accurate summation of numbers with the same sign irrespective of input order; + * Reduce cancellation effects that occur when large magnitude numbers with opposite signs are summed together with relatively small values; - * Provide accurate summation of numbers with the same sign irrespective of input order. + * Compute using two sums the terms of the same sign and the final result by combining + the sums (using add or subtract). []