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
The following commit(s) were added to refs/heads/master by this push: new ae6a5e3 NUMBERS-147: Fix conversion from double to support 2^31 ae6a5e3 is described below commit ae6a5e39b4e4c424959fbfdd9dc13aa05c3e3517 Author: Alex Herbert <aherb...@apache.org> AuthorDate: Thu Apr 16 22:32:15 2020 +0100 NUMBERS-147: Fix conversion from double to support 2^31 The value 2^31 can be in the numerator or denominator. Previously the conversion from double supported up to Integer.MAX_VALUE which is 2^31-1. Adds common test cases for conversion from double with a max denominator. Changed the conversion from a double to use the absolute value and restore the sign at the end. The conversion is thus identical for positive or negative values. Throw an illegal argument exception if maxDenominator is zero. A zero invalidates the conversion from a double with a restricted denominator. Replace Math.abs with compareUnsigned. This is appropriate as the fraction p/q is always positive. Add test cases for overflow double conversions. Fix the fall-back to always use p1/q1 when either p2 or q2 overflow. Move the default max iterations to a constant. Validate epsilon and max iterations are positive. --- .../commons/numbers/fraction/BigFraction.java | 98 ++++++++++++----- .../apache/commons/numbers/fraction/Fraction.java | 119 +++++++++++++++------ .../commons/numbers/fraction/BigFractionTest.java | 45 ++++++-- .../commons/numbers/fraction/CommonTestCases.java | 90 ++++++++++++++++ .../commons/numbers/fraction/FractionTest.java | 41 +++++-- 5 files changed, 314 insertions(+), 79 deletions(-) diff --git a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java index 996899f..a172d44 100644 --- a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java +++ b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/BigFraction.java @@ -47,9 +47,15 @@ public final class BigFraction /** Serializable version identifier. */ private static final long serialVersionUID = 20190701L; + /** The default iterations used for convergence. */ + private static final int DEFAULT_MAX_ITERATIONS = 100; + /** Message for non-finite input double argument to factory constructors. */ private static final String NOT_FINITE = "Not finite: "; + /** The overflow limit for conversion from a double (2^31). */ + private static final long OVERFLOW = 1L << 31; + /** The numerator of this fraction reduced to lowest terms. */ private final BigInteger numerator; @@ -142,21 +148,28 @@ public final class BigFraction return ZERO; } - final long overflow = Integer.MAX_VALUE; - double r0 = value; + // Remove sign, this is restored at the end. + // (Assumes the value is not zero and thus signum(value) is not zero). + final double absValue = Math.abs(value); + double r0 = absValue; long a0 = (long) Math.floor(r0); - - if (Math.abs(a0) > overflow) { - throw new FractionException(FractionException.ERROR_CONVERSION_OVERFLOW, value, a0, 1L); + if (a0 > OVERFLOW) { + throw new FractionException(FractionException.ERROR_CONVERSION_OVERFLOW, value, a0, 1); } - // check for (almost) integer arguments, which should not go - // to iterations. - if (Math.abs(a0 - value) <= epsilon) { - return new BigFraction(BigInteger.valueOf(a0), - BigInteger.ONE); + // check for (almost) integer arguments, which should not go to iterations. + if (r0 - a0 <= epsilon) { + // Restore the sign. + if (value < 0) { + a0 = -a0; + } + return new BigFraction(BigInteger.valueOf(a0)); } + // Support 2^31 as maximum denominator. + // This is negative as an integer so convert to long. + final long maxDen = Math.abs((long) maxDenominator); + long p0 = 1; long q0 = 0; long p1 = a0; @@ -169,16 +182,16 @@ public final class BigFraction boolean stop = false; do { ++n; - final double r1 = 1d / (r0 - a0); + final double r1 = 1.0 / (r0 - a0); final long a1 = (long) Math.floor(r1); p2 = (a1 * p1) + p0; q2 = (a1 * q1) + q0; - if (p2 > overflow || - q2 > overflow) { - // in maxDenominator mode, if the last fraction was very close to the actual value - // q2 may overflow in the next iteration; in this case return the last one. - if (epsilon == 0 && - Math.abs(q1) < maxDenominator) { + if (Long.compareUnsigned(p2, OVERFLOW) > 0 || + Long.compareUnsigned(q2, OVERFLOW) > 0) { + // In maxDenominator mode, fall-back to the previous valid fraction. + if (epsilon == 0) { + p2 = p1; + q2 = q1; break; } throw new FractionException(FractionException.ERROR_CONVERSION_OVERFLOW, value, p2, q2); @@ -186,8 +199,8 @@ public final class BigFraction final double convergent = (double) p2 / (double) q2; if (n < maxIterations && - Math.abs(convergent - value) > epsilon && - q2 < maxDenominator) { + Math.abs(convergent - absValue) > epsilon && + q2 < maxDen) { p0 = p1; p1 = p2; q0 = q1; @@ -203,11 +216,24 @@ public final class BigFraction throw new FractionException(FractionException.ERROR_CONVERSION, value, maxIterations); } - return q2 < maxDenominator ? - new BigFraction(BigInteger.valueOf(p2), - BigInteger.valueOf(q2)) : - new BigFraction(BigInteger.valueOf(p1), - BigInteger.valueOf(q1)); + // Use p2 / q2 or p1 / q1 if q2 has grown too large in maxDenominator mode + long num; + long den; + if (q2 <= maxDen) { + num = p2; + den = q2; + } else { + num = p1; + den = q1; + } + + // Restore the sign. + if (Math.signum(num) * Math.signum(den) != Math.signum(value)) { + num = -num; + } + + return new BigFraction(BigInteger.valueOf(num), + BigInteger.valueOf(den)); } /** @@ -290,18 +316,26 @@ public final class BigFraction * @param epsilon Maximum error allowed. The resulting fraction is within * {@code epsilon} of {@code value}, in absolute terms. * @param maxIterations Maximum number of convergents. - * @throws IllegalArgumentException if the given {@code value} is NaN or infinite. + * @throws IllegalArgumentException if the given {@code value} is NaN or infinite; + * {@code epsilon} is not positive; or {@code maxIterations < 1}. * @throws ArithmeticException if the continued fraction failed to converge. * @return a new instance. */ public static BigFraction from(final double value, final double epsilon, final int maxIterations) { - return from(value, epsilon, Integer.MAX_VALUE, maxIterations); + if (maxIterations < 1) { + throw new IllegalArgumentException("Max iterations must be strictly positive: " + maxIterations); + } + if (epsilon >= 0) { + return from(value, epsilon, Integer.MIN_VALUE, maxIterations); + } + throw new IllegalArgumentException("Epsilon must be positive: " + maxIterations); } /** * Create a fraction given the double value and maximum denominator. + * * <p> * References: * <ul> @@ -309,15 +343,23 @@ public final class BigFraction * Continued Fraction</a> equations (11) and (22)-(26)</li> * </ul> * + * <p>Note: The magnitude of the {@code maxDenominator} is used allowing use of + * {@link Integer#MIN_VALUE} for a supported maximum denominator of 2<sup>31</sup>. + * * @param value Value to convert to a fraction. * @param maxDenominator Maximum allowed value for denominator. - * @throws IllegalArgumentException if the given {@code value} is NaN or infinite. + * @throws IllegalArgumentException if the given {@code value} is NaN or infinite + * or {@code maxDenominator} is zero. * @throws ArithmeticException if the continued fraction failed to converge. * @return a new instance. */ public static BigFraction from(final double value, final int maxDenominator) { - return from(value, 0, maxDenominator, 100); + if (maxDenominator == 0) { + // Re-use the zero denominator message + throw new IllegalArgumentException(FractionException.ERROR_ZERO_DENOMINATOR); + } + return from(value, 0, maxDenominator, DEFAULT_MAX_ITERATIONS); } /** diff --git a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java index 1974f1e..f1bc542 100644 --- a/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java +++ b/commons-numbers-fraction/src/main/java/org/apache/commons/numbers/fraction/Fraction.java @@ -47,9 +47,15 @@ public final class Fraction /** The default epsilon used for convergence. */ private static final double DEFAULT_EPSILON = 1e-5; + /** The default iterations used for convergence. */ + private static final int DEFAULT_MAX_ITERATIONS = 100; + /** Message for non-finite input double argument to factory constructors. */ private static final String NOT_FINITE = "Not finite: "; + /** The overflow limit for conversion from a double (2^31). */ + private static final long OVERFLOW = 1L << 31; + /** The numerator of this fraction reduced to lowest terms. */ private final int numerator; @@ -136,7 +142,11 @@ public final class Fraction * https://issues.apache.org/jira/browse/MATH-181 * </p> * - * @param value Value to convert to a fraction. + * <p> + * Warning: This conversion assumes the value is not zero. + * </p> + * + * @param value Value to convert to a fraction. Must not be zero. * @param epsilon Maximum error allowed. * The resulting fraction is within {@code epsilon} of {@code value}, * in absolute terms. @@ -153,20 +163,36 @@ public final class Fraction throw new IllegalArgumentException(NOT_FINITE + value); } - final long overflow = Integer.MAX_VALUE; - double r0 = value; - long a0 = (long)Math.floor(r0); - if (Math.abs(a0) > overflow) { - throw new FractionException(FractionException.ERROR_CONVERSION_OVERFLOW, value, a0, 1L); + // Remove sign, this is restored at the end. + // (Assumes the value is not zero and thus signum(value) is not zero). + final double absValue = Math.abs(value); + double r0 = absValue; + long a0 = (long) Math.floor(r0); + if (a0 > OVERFLOW) { + throw new FractionException(FractionException.ERROR_CONVERSION_OVERFLOW, value, a0, 1); } // check for (almost) integer arguments, which should not go to iterations. - if (Math.abs(a0 - value) <= epsilon) { - this.numerator = (int) a0; - this.denominator = 1; + if (r0 - a0 <= epsilon) { + int num = (int) a0; + int den = 1; + // Restore the sign. + if (Math.signum(num) != Math.signum(value)) { + if (num == Integer.MIN_VALUE) { + den = -den; + } else { + num = -num; + } + } + this.numerator = num; + this.denominator = den; return; } + // Support 2^31 as maximum denominator. + // This is negative as an integer so convert to long. + final long maxDen = Math.abs((long) maxDenominator); + long p0 = 1; long q0 = 0; long p1 = a0; @@ -180,25 +206,25 @@ public final class Fraction do { ++n; final double r1 = 1.0 / (r0 - a0); - final long a1 = (long)Math.floor(r1); + final long a1 = (long) Math.floor(r1); p2 = (a1 * p1) + p0; q2 = (a1 * q1) + q0; - if (Math.abs(p2) > overflow || - Math.abs(q2) > overflow) { - // in maxDenominator mode, if the last fraction was very close to the actual value - // q2 may overflow in the next iteration; in this case return the last one. - if (epsilon == 0.0 && - Math.abs(q1) < maxDenominator) { + if (Long.compareUnsigned(p2, OVERFLOW) > 0 || + Long.compareUnsigned(q2, OVERFLOW) > 0) { + // In maxDenominator mode, fall-back to the previous valid fraction. + if (epsilon == 0.0) { + p2 = p1; + q2 = q1; break; } throw new FractionException(FractionException.ERROR_CONVERSION_OVERFLOW, value, p2, q2); } - final double convergent = (double)p2 / (double)q2; + final double convergent = (double) p2 / (double) q2; if (n < maxIterations && - Math.abs(convergent - value) > epsilon && - q2 < maxDenominator) { + Math.abs(convergent - absValue) > epsilon && + q2 < maxDen) { p0 = p1; p1 = p2; q0 = q1; @@ -214,13 +240,30 @@ public final class Fraction throw new FractionException(FractionException.ERROR_CONVERSION, value, maxIterations); } - if (q2 < maxDenominator) { - this.numerator = (int) p2; - this.denominator = (int) q2; + // Use p2 / q2 or p1 / q1 if q2 has grown too large in maxDenominator mode + // Note: Conversion of long 2^31 to an integer will create a negative. This could + // be either the numerator or denominator. This is handled by restoring the sign. + int num; + int den; + if (q2 <= maxDen) { + num = (int) p2; + den = (int) q2; } else { - this.numerator = (int) p1; - this.denominator = (int) q1; + num = (int) p1; + den = (int) q1; } + + // Restore the sign. + if (Math.signum(num) * Math.signum(den) != Math.signum(value)) { + if (num == Integer.MIN_VALUE) { + den = -den; + } else { + num = -num; + } + } + + this.numerator = num; + this.denominator = den; } /** @@ -228,12 +271,11 @@ public final class Fraction * * @param value Value to convert to a fraction. * @throws IllegalArgumentException if the given {@code value} is NaN or infinite. - * @throws ArithmeticException if the continued fraction failed to - * converge. + * @throws ArithmeticException if the continued fraction failed to converge. * @return a new instance. */ public static Fraction from(final double value) { - return from(value, DEFAULT_EPSILON, 100); + return from(value, DEFAULT_EPSILON, DEFAULT_MAX_ITERATIONS); } /** @@ -250,7 +292,8 @@ public final class Fraction * @param epsilon Maximum error allowed. The resulting fraction is within * {@code epsilon} of {@code value}, in absolute terms. * @param maxIterations Maximum number of convergents. - * @throws IllegalArgumentException if the given {@code value} is NaN or infinite. + * @throws IllegalArgumentException if the given {@code value} is NaN or infinite; + * {@code epsilon} is not positive; or {@code maxIterations < 1}. * @throws ArithmeticException if the continued fraction failed to converge. * @return a new instance. */ @@ -260,7 +303,13 @@ public final class Fraction if (value == 0) { return ZERO; } - return new Fraction(value, epsilon, Integer.MAX_VALUE, maxIterations); + if (maxIterations < 1) { + throw new IllegalArgumentException("Max iterations must be strictly positive: " + maxIterations); + } + if (epsilon >= 0) { + return new Fraction(value, epsilon, Integer.MIN_VALUE, maxIterations); + } + throw new IllegalArgumentException("Epsilon must be positive: " + maxIterations); } /** @@ -273,9 +322,13 @@ public final class Fraction * Continued Fraction</a> equations (11) and (22)-(26)</li> * </ul> * + * <p>Note: The magnitude of the {@code maxDenominator} is used allowing use of + * {@link Integer#MIN_VALUE} for a supported maximum denominator of 2<sup>31</sup>. + * * @param value Value to convert to a fraction. * @param maxDenominator Maximum allowed value for denominator. - * @throws IllegalArgumentException if the given {@code value} is NaN or infinite. + * @throws IllegalArgumentException if the given {@code value} is NaN or infinite + * or {@code maxDenominator} is zero. * @throws ArithmeticException if the continued fraction failed to converge. * @return a new instance. */ @@ -284,7 +337,11 @@ public final class Fraction if (value == 0) { return ZERO; } - return new Fraction(value, 0, maxDenominator, 100); + if (maxDenominator == 0) { + // Re-use the zero denominator message + throw new IllegalArgumentException(FractionException.ERROR_ZERO_DENOMINATOR); + } + return new Fraction(value, 0, maxDenominator, DEFAULT_MAX_ITERATIONS); } /** diff --git a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java index add8efd..f0bcd27 100644 --- a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java +++ b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/BigFractionTest.java @@ -146,23 +146,29 @@ public class BigFractionTest { } // MATH-181 + // NUMBERS-147 @Test public void testDoubleConstructorWithMaxDenominator() throws Exception { - assertFraction(2, 5, BigFraction.from(0.4, 9)); - assertFraction(2, 5, BigFraction.from(0.4, 99)); - assertFraction(2, 5, BigFraction.from(0.4, 999)); + for (final CommonTestCases.DoubleToFractionTestCase testCase : CommonTestCases.doubleMaxDenomConstructorTestCases()) { + assertFraction( + testCase.expectedNumerator, + testCase.expectedDenominator, + BigFraction.from(testCase.operand, testCase.maxDenominator) + ); + } - assertFraction(3, 5, BigFraction.from(0.6152, 9)); - assertFraction(8, 13, BigFraction.from(0.6152, 99)); - assertFraction(510, 829, BigFraction.from(0.6152, 999)); - assertFraction(769, 1250, BigFraction.from(0.6152, 9999)); + // Cases with different exact results from Fraction + final long pow31 = 1L << 31; + assertFraction(pow31, 1, BigFraction.from(Integer.MIN_VALUE * -1.0, 2)); + assertFraction(pow31, 3, BigFraction.from(Integer.MIN_VALUE / -3.0, 10)); + assertFraction(-1, pow31, BigFraction.from(1.0 / Integer.MIN_VALUE, Integer.MIN_VALUE)); + assertFraction(1, pow31, BigFraction.from(-1.0 / Integer.MIN_VALUE, Integer.MIN_VALUE)); - // MATH-996 - assertFraction(1, 2, BigFraction.from(0.5000000001, 10)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BigFraction.from(1.0, 0)); } @Test - public void testDoubleConstructorThrowsWithNonFinite() { + public void testDoubleConstructorThrows() { final double eps = 1e-5; final int maxIterations = Integer.MAX_VALUE; final int maxDenominator = Integer.MAX_VALUE; @@ -171,6 +177,11 @@ public class BigFractionTest { Assertions.assertThrows(IllegalArgumentException.class, () -> BigFraction.from(value, eps, maxIterations)); Assertions.assertThrows(IllegalArgumentException.class, () -> BigFraction.from(value, maxDenominator)); } + Assertions.assertThrows(IllegalArgumentException.class, () -> BigFraction.from(1.0, Double.NaN, maxIterations)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BigFraction.from(1.0, -1.0, maxIterations)); + Assertions.assertThrows(IllegalArgumentException.class, () -> BigFraction.from(1.0, eps, 0)); + // Test a zero epsilon is allowed + assertFraction(1, 1, BigFraction.from(1.0, 0, maxIterations)); } @Test @@ -193,6 +204,20 @@ public class BigFractionTest { } @Test + public void testDoubleConstructorOverflow() { + assertDoubleConstructorOverflow(0.75000000001455192); + assertDoubleConstructorOverflow(1.0e10); + assertDoubleConstructorOverflow(-1.0e10); + assertDoubleConstructorOverflow(-43979.60679604749); + } + + private void assertDoubleConstructorOverflow(final double a) { + Assertions.assertThrows(ArithmeticException.class, + () -> BigFraction.from(a, 1.0e-12, 1000) + ); + } + + @Test public void testDoubleConstructorWithEpsilonLimit() throws Exception { assertFraction(2, 5, BigFraction.from(0.4, 1.0e-5, 100)); diff --git a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/CommonTestCases.java b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/CommonTestCases.java index 59bd68e..d6e58dc 100644 --- a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/CommonTestCases.java +++ b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/CommonTestCases.java @@ -38,6 +38,11 @@ final class CommonTestCases { private static final List<DoubleToFractionTestCase> doubleConstructorTestCasesList; /** + * See {@link #doubleMaxDenomConstructorTestCases()} + */ + private static final List<DoubleToFractionTestCase> doubleMaxDenomConstructorTestCasesList; + + /** * See {@link #absTestCases()} */ private static final List<UnaryOperatorTestCase> absTestCasesList; @@ -100,6 +105,7 @@ final class CommonTestCases { static { numDenConstructorTestCasesList = collectNumDenConstructorTestCases(); doubleConstructorTestCasesList = collectDoubleConstructorTestCases(); + doubleMaxDenomConstructorTestCasesList = collectDoubleMaxDenomConstructorTestCases(); absTestCasesList = collectAbsTestCases(); reciprocalTestCasesList = collectReciprocalTestCases(); negateTestCasesList = collectNegateTestCases(); @@ -206,6 +212,63 @@ final class CommonTestCases { } /** + * Defines test cases as described in + * {@link #doubleMaxDenomConstructorTestCases()} and collects them into a {@code List}. + * @return a list of test cases as described above + */ + private static List<DoubleToFractionTestCase> collectDoubleMaxDenomConstructorTestCases() { + final List<DoubleToFractionTestCase> testCases = new ArrayList<>(); + testCases.add(new DoubleToFractionTestCase(0.4, 9, 2, 5)); + testCases.add(new DoubleToFractionTestCase(0.4, 99, 2, 5)); + testCases.add(new DoubleToFractionTestCase(0.4, 999, 2, 5)); + testCases.add(new DoubleToFractionTestCase(-0.4, 9, -2, 5)); + testCases.add(new DoubleToFractionTestCase(-0.4, 99, -2, 5)); + testCases.add(new DoubleToFractionTestCase(-0.4, 999, -2, 5)); + + testCases.add(new DoubleToFractionTestCase(0.6152, 9, 3, 5)); + testCases.add(new DoubleToFractionTestCase(0.6152, 99, 8, 13)); + testCases.add(new DoubleToFractionTestCase(0.6152, 999, 510, 829)); + testCases.add(new DoubleToFractionTestCase(0.6152, 9999, 769, 1250)); + testCases.add(new DoubleToFractionTestCase(-0.6152, 9, -3, 5)); + testCases.add(new DoubleToFractionTestCase(-0.6152, 99, -8, 13)); + testCases.add(new DoubleToFractionTestCase(-0.6152, 999, -510, 829)); + testCases.add(new DoubleToFractionTestCase(-0.6152, 9999, -769, 1250)); + + // Underflow + testCases.add(new DoubleToFractionTestCase(0x1.0p-40, Integer.MAX_VALUE, 0, 1)); + testCases.add(new DoubleToFractionTestCase(-0x1.0p-40, Integer.MAX_VALUE, 0, 1)); + + // Overflow + testCases.add(new DoubleToFractionTestCase(Math.nextUp((double) Integer.MAX_VALUE), Integer.MIN_VALUE, Integer.MAX_VALUE, 1)); + testCases.add(new DoubleToFractionTestCase(-Math.nextUp((double) Integer.MAX_VALUE), Integer.MIN_VALUE, -Integer.MAX_VALUE, 1)); + testCases.add(new DoubleToFractionTestCase(Math.nextUp((double) Integer.MAX_VALUE) / (1 << 15), Integer.MIN_VALUE, Integer.MAX_VALUE, 1 << 15)); + testCases.add(new DoubleToFractionTestCase(-Math.nextUp((double) Integer.MAX_VALUE) / (1 << 15), Integer.MIN_VALUE, -Integer.MAX_VALUE, 1 << 15)); + testCases.add(new DoubleToFractionTestCase(Math.nextUp(1.0), Integer.MIN_VALUE, 1, 1)); + testCases.add(new DoubleToFractionTestCase(-Math.nextUp(1.0), Integer.MIN_VALUE, -1, 1)); + + // MATH-996 + testCases.add(new DoubleToFractionTestCase(0.5000000001, 10, 1, 2)); + testCases.add(new DoubleToFractionTestCase(-0.5000000001, 10, -1, 2)); + + // NUMBERS-147 + testCases.add(new DoubleToFractionTestCase(Integer.MAX_VALUE * 1.0, 2, Integer.MAX_VALUE, 1)); + testCases.add(new DoubleToFractionTestCase(Integer.MAX_VALUE * -1.0, 2, -Integer.MAX_VALUE, 1)); + testCases.add(new DoubleToFractionTestCase(1.0 / Integer.MAX_VALUE, Integer.MAX_VALUE, 1, Integer.MAX_VALUE)); + testCases.add(new DoubleToFractionTestCase(-1.0 / Integer.MAX_VALUE, Integer.MAX_VALUE, -1, Integer.MAX_VALUE)); + testCases.add(new DoubleToFractionTestCase(Integer.MIN_VALUE * 1.0, 2, Integer.MIN_VALUE, 1)); + + // Using 2^31: + // Representations in Fraction and BigFraction are different since BigFraction + // can use +2^31 but Fraction is limited to -2^31 + // .from(Integer.MIN_VALUE * -1.0, 2) + // .from(Integer.MIN_VALUE / -3.0, Integer.MIN_VALUE) + // .from(1.0 / Integer.MIN_VALUE, Integer.MIN_VALUE) + // .from(-1.0 / Integer.MIN_VALUE, Integer.MIN_VALUE) + + return testCases; + } + + /** * Defines test cases as described in {@link #absTestCases()} and * collects them into a {@code List}. * @return a list of test cases as described above @@ -528,6 +591,8 @@ final class CommonTestCases { * allowed, and the expected numerator and denominator of the resulting * fraction are in the {@code int} range. * + * <p>The maximum denominator in the test cases will be zero and should be ignored. + * * @return a list of test cases as described above */ static List<DoubleToFractionTestCase> doubleConstructorTestCases() { @@ -535,6 +600,18 @@ final class CommonTestCases { } /** + * Provides a list of test cases where a {@code double} value should be + * converted to a fraction with a specified maximum denominator, and the + * expected numerator and denominator of the resulting fraction are in the + * {@code int} range. + * + * @return a list of test cases as described above + */ + static List<DoubleToFractionTestCase> doubleMaxDenomConstructorTestCases() { + return Collections.unmodifiableList(doubleMaxDenomConstructorTestCasesList); + } + + /** * Provides a list of test cases where the absolute value of a fraction * created from a specified numerator and denominator, both in the {@code * int} range, should be calculated, and the expected numerator and @@ -791,19 +868,32 @@ final class CommonTestCases { * should be performed on a {@code double} value and the numerator and * denominator of the expected result * are in the {@code int} range. + * + * <p>Optionally captures a maximum denominator. This will be zero if + * not required in the test case. */ static class DoubleToFractionTestCase { final double operand; + final int maxDenominator; final int expectedNumerator; final int expectedDenominator; DoubleToFractionTestCase( double operand, + int maxDenominator, int expectedNumerator, int expectedDenominator) { this.operand = operand; + this.maxDenominator = maxDenominator; this.expectedNumerator = expectedNumerator; this.expectedDenominator = expectedDenominator; } + + DoubleToFractionTestCase( + double operand, + int expectedNumerator, + int expectedDenominator) { + this(operand, 0, expectedNumerator, expectedDenominator); + } } } diff --git a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java index 80628fa..f26a616 100644 --- a/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java +++ b/commons-numbers-fraction/src/test/java/org/apache/commons/numbers/fraction/FractionTest.java @@ -99,23 +99,28 @@ public class FractionTest { } // MATH-181 + // NUMBERS-147 @Test public void testDoubleConstructorWithMaxDenominator() throws Exception { - assertFraction(2, 5, Fraction.from(0.4, 9)); - assertFraction(2, 5, Fraction.from(0.4, 99)); - assertFraction(2, 5, Fraction.from(0.4, 999)); + for (final CommonTestCases.DoubleToFractionTestCase testCase : CommonTestCases.doubleMaxDenomConstructorTestCases()) { + assertFraction( + testCase.expectedNumerator, + testCase.expectedDenominator, + Fraction.from(testCase.operand, testCase.maxDenominator) + ); + } - assertFraction(3, 5, Fraction.from(0.6152, 9)); - assertFraction(8, 13, Fraction.from(0.6152, 99)); - assertFraction(510, 829, Fraction.from(0.6152, 999)); - assertFraction(769, 1250, Fraction.from(0.6152, 9999)); + // Cases with different exact results from BigFraction + assertFraction(Integer.MIN_VALUE, -1, Fraction.from(Integer.MIN_VALUE * -1.0, 2)); + assertFraction(Integer.MIN_VALUE, -3, Fraction.from(Integer.MIN_VALUE / -3.0, 10)); + assertFraction(1, Integer.MIN_VALUE, Fraction.from(1.0 / Integer.MIN_VALUE, Integer.MIN_VALUE)); + assertFraction(-1, Integer.MIN_VALUE, Fraction.from(-1.0 / Integer.MIN_VALUE, Integer.MIN_VALUE)); - // MATH-996 - assertFraction(1, 2, Fraction.from(0.5000000001, 10)); + Assertions.assertThrows(IllegalArgumentException.class, () -> Fraction.from(1.0, 0)); } @Test - public void testDoubleConstructorThrowsWithNonFinite() { + public void testDoubleConstructorThrows() { final double eps = 1e-5; final int maxIterations = Integer.MAX_VALUE; final int maxDenominator = Integer.MAX_VALUE; @@ -124,6 +129,11 @@ public class FractionTest { Assertions.assertThrows(IllegalArgumentException.class, () -> Fraction.from(value, eps, maxIterations)); Assertions.assertThrows(IllegalArgumentException.class, () -> Fraction.from(value, maxDenominator)); } + Assertions.assertThrows(IllegalArgumentException.class, () -> Fraction.from(1.0, Double.NaN, maxIterations)); + Assertions.assertThrows(IllegalArgumentException.class, () -> Fraction.from(1.0, -1.0, maxIterations)); + Assertions.assertThrows(IllegalArgumentException.class, () -> Fraction.from(1.0, eps, 0)); + // Test a zero epsilon is allowed + assertFraction(1, 1, Fraction.from(1.0, 0, maxIterations)); } @Test @@ -134,6 +144,17 @@ public class FractionTest { ); } + // MATH-1029 + @Test + public void testDoubleConstructorWithMaxDenominatorOverFlow() { + Assertions.assertThrows(ArithmeticException.class, + () -> Fraction.from(1e10, 1000) + ); + Assertions.assertThrows(ArithmeticException.class, + () -> Fraction.from(-1e10, 1000) + ); + } + @Test public void testDoubleConstructorOverflow() { assertDoubleConstructorOverflow(0.75000000001455192);