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 a3370a933dcf0c4a6e2f7f2519ea610f92704fe6 Author: aherbert <aherb...@apache.org> AuthorDate: Tue Nov 2 10:48:35 2021 +0000 NUMBERS-173: Set minimum bound on continued fraction epsilon --- .../numbers/fraction/ContinuedFraction.java | 12 +++++++--- .../numbers/fraction/ContinuedFractionTest.java | 28 ++++++++++++++++++++++ src/changes/changes.xml | 4 ++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/ContinuedFraction.java b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/ContinuedFraction.java index 3db48d6..38d1708 100644 --- a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/ContinuedFraction.java +++ b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/ContinuedFraction.java @@ -45,6 +45,8 @@ public abstract class ContinuedFraction { * eps * |b_n|, e.g., 1e-50". */ private static final double SMALL = 1e-50; + /** Minimum allowed relative error epsilon. Equal to Math.ulp(1.0). */ + private static final double MIN_EPSILON = 0x1.0p-52; /** * Defines the <a href="https://mathworld.wolfram.com/ContinuedFraction.html"> @@ -70,7 +72,7 @@ public abstract class ContinuedFraction { * Evaluates the continued fraction. * * @param x the evaluation point. - * @param epsilon Maximum error allowed. + * @param epsilon Maximum relative error allowed. * @return the value of the continued fraction evaluated at {@code x}. * @throws ArithmeticException if the algorithm fails to converge. * @throws ArithmeticException if the maximal number of iterations is reached @@ -100,7 +102,7 @@ public abstract class ContinuedFraction { * </ul> * * @param x Point at which to evaluate the continued fraction. - * @param epsilon Maximum error allowed. + * @param epsilon Maximum relative error allowed. * @param maxIterations Maximum number of iterations. * @return the value of the continued fraction evaluated at {@code x}. * @throws ArithmeticException if the algorithm fails to converge. @@ -108,6 +110,10 @@ public abstract class ContinuedFraction { * before the expected convergence is achieved. */ public double evaluate(double x, double epsilon, int maxIterations) { + // Relative error epsilon must not be zero. + // Do not use Math.max as NaN would be returned for a NaN epsilon. + final double eps = epsilon > MIN_EPSILON ? epsilon : MIN_EPSILON; + double hPrev = updateIfCloseToZero(getB(0, x)); int n = 1; @@ -131,7 +137,7 @@ public abstract class ContinuedFraction { "Continued fraction diverged to %s for value %s", hN, x); } - if (Math.abs(deltaN - 1) < epsilon) { + if (Math.abs(deltaN - 1) < eps) { return hN; } diff --git a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/ContinuedFractionTest.java b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/ContinuedFractionTest.java index caa947c..79bf890 100644 --- a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/ContinuedFractionTest.java +++ b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/ContinuedFractionTest.java @@ -19,11 +19,15 @@ package org.apache.commons.numbers.fraction; import java.util.Locale; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests for {@link ContinuedFraction}. */ class ContinuedFractionTest { + /** Golden ratio constant. */ + private static final double GOLDEN_RATIO = 1.618033988749894848204586834365638117720309; @Test void testGoldenRatio() throws Exception { @@ -44,6 +48,30 @@ class ContinuedFractionTest { Assertions.assertEquals(1.61803399, gr, eps); } + /** + * Test an invalid epsilon (zero, negative or NaN). + * See NUMBERS-173. + * + * @param epsilon the epsilon + */ + @ParameterizedTest + @ValueSource(doubles = {0, -1, Double.NaN}) + void testGoldenRatioEpsilonZero(double epsilon) { + ContinuedFraction cf = new ContinuedFraction() { + @Override + public double getA(int n, double x) { + return 1; + } + + @Override + public double getB(int n, double x) { + return 1; + } + }; + + Assertions.assertEquals(GOLDEN_RATIO, cf.evaluate(0, epsilon), Math.ulp(GOLDEN_RATIO)); + } + @Test void test415Over93() throws Exception { // https://en.wikipedia.org/wiki/Continued_fraction diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f4d27df..d3baf40 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -72,6 +72,10 @@ N.B. the Performance testing module requires Java 9+. (The unit tests require Java 8+) "> + <action dev="aherbert" type="fix" issue="173"> + "ContinuedFraction": Set a minimum bound on the relative error epsilon. Prevents + an infinite loop when the epsilon is zero. + </action> <action dev="aherbert" type="fix"> "FactorialDouble": Prevent caching values that are infinite. The cache will support factorials up to 170.