Improve Fraction Javadoc, implementation and tests

bug 22386, from Phil Steitz


git-svn-id: https://svn.apache.org/repos/asf/jakarta/commons/proper/lang/trunk@137586 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Stephen Colebourne 2003-08-13 23:42:17 +00:00
parent 20c35a1d87
commit 3e2d12ad71
2 changed files with 228 additions and 70 deletions

View File

@ -66,7 +66,7 @@ import java.io.Serializable;
* @author Stephen Colebourne
* @author Tim O'Brien
* @since 2.0
* @version $Id: Fraction.java,v 1.8 2003/08/04 02:01:53 scolebourne Exp $
* @version $Id: Fraction.java,v 1.9 2003/08/13 23:42:17 scolebourne Exp $
*/
public final class Fraction extends Number implements Serializable, Comparable {
@ -161,6 +161,8 @@ public final class Fraction extends Number implements Serializable, Comparable {
* @throws ArithmeticException if the denomiator is <code>zero</code>
* @throws ArithmeticException if the denomiator is negative
* @throws ArithmeticException if the numerator is negative
* @throws ArithmeticException if the resulting numerator exceeds
* <code>Integer.MAX_VALUE</code>
*/
public static Fraction getFraction(int whole, int numerator, int denominator) {
if (denominator == 0) {
@ -172,12 +174,16 @@ public final class Fraction extends Number implements Serializable, Comparable {
if (numerator < 0) {
throw new ArithmeticException("The numerator must not be negative");
}
double numeratorValue = 0;
if (whole < 0) {
numerator = whole * denominator - numerator;
numeratorValue = (double) whole * denominator - numerator;
} else {
numerator = whole * denominator + numerator;
numeratorValue = (double) whole * denominator + numerator;
}
return new Fraction(numerator, denominator);
if (Math.abs(numeratorValue) > Integer.MAX_VALUE) {
throw new ArithmeticException("Numerator too large to represent as an Integer.");
}
return new Fraction((int) numeratorValue, denominator);
}
/**
@ -199,30 +205,34 @@ public final class Fraction extends Number implements Serializable, Comparable {
numerator = -numerator;
denominator = -denominator;
}
int gcd = greatestCommonDenominator(Math.abs(numerator), denominator);
int gcd = greatestCommonDivisor(Math.abs(numerator), denominator);
return new Fraction(numerator / gcd, denominator / gcd);
}
/**
* <p>Creates a <code>Fraction</code> instance from a <code>double</code> value.</p>
*
* <p>This method uses the continued fraction algorithm.</p>
* <p>This method uses the <a href="http://archives.math.utk.edu/articles/atuyl/confrac/">
* continued fraction algorithm</a>, computing a maximum of
* 25 convergents and bounding the denominator by 10,000.</p>
*
* @param value the double value to convert
* @return a new fraction instance that is close to the value
* @throws ArithmeticException if the value is infinite or <code>NaN</code>
* @throws ArithmeticException if <code>|value| > Integer.MAX_VALUE</code>
* or <code>value = NaN</code>
* @throws ArithmeticException if the calculated denomiator is <code>zero</code>
* @throws ArithmeticException if the the algorithm does not converge
*/
public static Fraction getFraction(double value) {
if (Double.isInfinite(value) || Double.isNaN(value)) {
throw new ArithmeticException("The value must not be infinite or NaN");
}
int sign = (value < 0 ? -1 : 1);
value = Math.abs(value);
if (value > Integer.MAX_VALUE || Double.isNaN(value)) {
throw new ArithmeticException
("The value must not be greater than Integer.MAX_VALUE or NaN");
}
int wholeNumber = (int) value;
value -= wholeNumber;
// http://archives.math.utk.edu/articles/atuyl/confrac/
int numer0 = 0; // the pre-previous
int denom0 = 1; // the pre-previous
int numer1 = 1; // the previous
@ -430,7 +440,7 @@ public final class Fraction extends Number implements Serializable, Comparable {
* @return a new reduce fraction instance, or this if no simplification possible
*/
public Fraction reduce() {
int gcd = greatestCommonDenominator(Math.abs(numerator), denominator);
int gcd = greatestCommonDivisor(Math.abs(numerator), denominator);
return Fraction.getFraction(numerator / gcd, denominator / gcd);
}
@ -483,27 +493,39 @@ public final class Fraction extends Number implements Serializable, Comparable {
*
* @param power the power to raise the fraction to
* @return <code>this</code> if the power is one, <code>ONE</code> if the power
* is zero or a new fraction instance raised to the appropriate power
* is zero (even if the fraction equals ZERO) or a new fraction instance
* raised to the appropriate power
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* <code>Integer.MAX_VALUE</code>
*/
public Fraction pow(int power) {
if (power == 1) {
return this;
} else if (power == 0) {
return ONE;
} else if (power < 0) {
return getFraction((int) Math.pow(denominator, -power), (int) Math.pow(numerator, -power));
} else {
double denominatorValue = Math.pow(denominator, power);
double numeratorValue = Math.pow(numerator, power);
if (numeratorValue > Integer.MAX_VALUE || denominatorValue > Integer.MAX_VALUE) {
throw new ArithmeticException("Integer overflow");
}
if (power < 0) {
return getFraction((int) Math.pow(denominator, -power),
(int) Math.pow(numerator, -power));
}
return getFraction((int) Math.pow(numerator, power),
(int) Math.pow(denominator, power));
}
return getFraction((int) Math.pow(numerator, power), (int) Math.pow(denominator, power));
}
/**
* <p>Gets the greatest common denominator of two numbers.</p>
* <p>Gets the greatest common divisor of two numbers.</p>
*
* @param number1 a positive number
* @param number2 a positive number
* @return the greatest common denominator, never zero
* @return the greatest common divisor, never zero
*/
private static int greatestCommonDenominator(int number1, int number2) {
private static int greatestCommonDivisor(int number1, int number2) {
int remainder = number1 % number2;
while (remainder != 0) {
number1 = number2;
@ -517,15 +539,14 @@ public final class Fraction extends Number implements Serializable, Comparable {
//-------------------------------------------------------------------
/**
* <p>Adds the value of this fraction to another, returning the result.</p>
*
* <p>The implementation spots common cases of zero numerators and equal
* denominators. Otherwise, it uses <code>(a/b) + (c/d) = (a*d + b*c) / (b*d)</code>
* and then reduces the result.</p>
* <p>Adds the value of this fraction to another, returning the result in
* reduced form.</p>
*
* @param fraction the fraction to add, must not be <code>null</code>
* @return a <code>Fraction</code> instance with the resulting values
* @throws IllegalArgumentException if the fraction is <code>null</code>
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* <code>Integer.MAX_VALUE</code>
*/
public Fraction add(Fraction fraction) {
if (fraction == null) {
@ -536,56 +557,46 @@ public final class Fraction extends Number implements Serializable, Comparable {
}
if (fraction.numerator == 0) {
return this;
}
// Compute lcd explicitly to limit overflow
int gcd = greatestCommonDivisor(Math.abs(fraction.denominator), Math.abs(denominator));
int thisResidue = denominator/gcd;
int thatResidue = fraction.denominator/gcd;
double denominatorValue = Math.abs((double) gcd * thisResidue * thatResidue);
double numeratorValue = (double) numerator * thatResidue + fraction.numerator * thisResidue;
if (Math.abs(numeratorValue) > Integer.MAX_VALUE ||
Math.abs(denominatorValue) > Integer.MAX_VALUE) {
throw new ArithmeticException("Integer overflow");
}
if (denominator == fraction.denominator) {
return getReducedFraction(numerator + fraction.numerator, denominator);
}
return getReducedFraction(
numerator * fraction.denominator + denominator * fraction.numerator,
denominator * fraction.denominator
);
return Fraction.getReducedFraction((int) numeratorValue, (int) denominatorValue);
}
/**
* <p>Subtracts the value of another fraction from the value of this one,
* returning the result.</p>
*
* <p>The implementation spots common cases of zero numerators and equal
* denominators. Otherwise, it uses <code>(a/b) - (c/d) = (a*d - b*c) / (b*d)</code>
* and then reduces the result.</p>
* returning the result in reduced form.</p>
*
* @param fraction the fraction to subtract, must not be <code>null</code>
* @return a <code>Fraction</code> instance with the resulting values
* @throws IllegalArgumentException if the fraction is <code>null</code>
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* <code>Integer.MAX_VALUE</code>
*/
public Fraction subtract(Fraction fraction) {
if (fraction == null) {
throw new IllegalArgumentException("The fraction must not be null");
}
if (numerator == 0) {
return fraction.negate();
}
if (fraction.numerator == 0) {
return this;
}
if (denominator == fraction.denominator) {
return getReducedFraction(numerator - fraction.numerator, denominator);
}
return getReducedFraction(
numerator * fraction.denominator - denominator * fraction.numerator,
denominator * fraction.denominator
);
return add(fraction.negate());
}
/**
* <p>Multiplies the value of this fraction by another, returning the result.</p>
*
* <p>The implementation uses <code>(a/b)*(c/d) = (a*c)/(b*d)</code>
* and then reduces the result.</p>
* <p>Multiplies the value of this fraction by another, returning the result
* in reduced form.</p>
*
* @param fraction the fraction to multipy by, must not be <code>null</code>
* @return a <code>Fraction</code> instance with the resulting values
* @throws IllegalArgumentException if the fraction is <code>null</code>
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* <code>Integer.MAX_VALUE</code>
*/
public Fraction multiplyBy(Fraction fraction) {
if (fraction == null) {
@ -594,22 +605,25 @@ public final class Fraction extends Number implements Serializable, Comparable {
if (numerator == 0 || fraction.numerator == 0) {
return ZERO;
}
return getReducedFraction(
numerator * fraction.numerator,
denominator * fraction.denominator
);
double numeratorValue = (double) numerator * fraction.numerator;
double denominatorValue = (double) denominator * fraction.denominator;
if (Math.abs(numeratorValue) > Integer.MAX_VALUE ||
Math.abs(denominatorValue) > Integer.MAX_VALUE) {
throw new ArithmeticException("Integer overflow");
}
return getReducedFraction((int) numeratorValue, (int) denominatorValue);
}
/**
* <p>Divide the value of this fraction by another, returning the result.</p>
*
* <p>The implementation uses <code>(a/b)/(c/d) = a/b * d/c = (a*d)/(b*c)</code>
* and then reduces the result.</p>
* <p>Divide the value of this fraction by another, returning the result
* in reduced form.</p>
*
* @param fraction the fraction to divide by, must not be <code>null</code>
* @return a <code>Fraction</code> instance with the resulting values
* @throws IllegalArgumentException if the fraction is <code>null</code>
* @throws ArithmeticException if the fraction to divide by is zero
* @throws ArithmeticException if the resulting numerator or denominator exceeds
* <code>Integer.MAX_VALUE</code>
*/
public Fraction divideBy(Fraction fraction) {
if (fraction == null) {
@ -620,11 +634,14 @@ public final class Fraction extends Number implements Serializable, Comparable {
}
if (numerator == 0) {
return ZERO;
}
double numeratorValue = (double) numerator * fraction.denominator;
double denominatorValue = (double) denominator * fraction.numerator;
if (Math.abs(numeratorValue) > Integer.MAX_VALUE ||
Math.abs(denominatorValue) > Integer.MAX_VALUE) {
throw new ArithmeticException("Integer overflow");
}
return getReducedFraction(
numerator * fraction.denominator,
denominator * fraction.numerator
);
return getReducedFraction((int) numeratorValue, (int) denominatorValue);
}
// Basics
@ -668,7 +685,7 @@ public final class Fraction extends Number implements Serializable, Comparable {
* <p>Compares this object to another based on size.</p>
*
* @param object the object to compare to
* @return -ve if this is less, 0 if equal, +ve if greater
* @return -1 if this is less, 0 if equal, +1 if greater
* @throws ClassCastException if the object is not a <code>Fraction</code>
* @throws NullPointerException if the object is <code>null</code>
*/

View File

@ -60,11 +60,11 @@ import junit.framework.TestSuite;
* Test cases for the {@link Fraction} classes.
*
* @author Stephen Colebourne
* @version $Id: FractionTest.java,v 1.3 2003/08/04 02:01:53 scolebourne Exp $
* @version $Id: FractionTest.java,v 1.4 2003/08/13 23:42:17 scolebourne Exp $
*/
public class FractionTest extends TestCase {
private static final int SKIP = 17;
private static final int SKIP = 53;
public FractionTest(String name) {
super(name);
@ -165,14 +165,17 @@ public class FractionTest extends TestCase {
// zero denominator
try {
f = Fraction.getFraction(1, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(2, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(-3, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
}
@ -200,14 +203,17 @@ public class FractionTest extends TestCase {
// negatives
try {
f = Fraction.getFraction(1, -6, -10);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(1, -6, -10);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(1, -6, -10);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
// negative whole
@ -217,27 +223,43 @@ public class FractionTest extends TestCase {
try {
f = Fraction.getFraction(-1, -6, 10);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(-1, 6, -10);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(-1, -6, -10);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
// zero denominator
try {
f = Fraction.getFraction(0, 1, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(1, 2, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(-1, -3, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(Integer.MAX_VALUE, 1, 2);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(-Integer.MAX_VALUE, 1, 2);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
}
@ -279,14 +301,17 @@ public class FractionTest extends TestCase {
// zero denominator
try {
f = Fraction.getReducedFraction(1, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getReducedFraction(2, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getReducedFraction(-3, 0);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
// reduced
@ -316,14 +341,22 @@ public class FractionTest extends TestCase {
try {
f = Fraction.getFraction(Double.NaN);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(Double.POSITIVE_INFINITY);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction(Double.NEGATIVE_INFINITY);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f = Fraction.getFraction((double) Integer.MAX_VALUE + 1);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
// zero
@ -377,7 +410,7 @@ public class FractionTest extends TestCase {
assertEquals(f2.getDenominator(), f.getDenominator());
}
}
// save time by skipping some tests!
// save time by skipping some tests! (
for (int i = 1001; i <= 10000; i+=SKIP) { // denominator
for (int j = 1; j <= i; j++) { // numerator
try {
@ -396,9 +429,11 @@ public class FractionTest extends TestCase {
public void testFactory_String() {
try {
Fraction.getFraction(null);
fail("expecting ArithmeticException");
} catch (IllegalArgumentException ex) {}
}
public void testFactory_String_double() {
Fraction f = null;
@ -422,6 +457,11 @@ public class FractionTest extends TestCase {
f = Fraction.getFraction("2.3R");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("2147483648"); // too big
fail("Expecting NumberFormatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction(".");
} catch (NumberFormatException ex) {}
@ -448,26 +488,32 @@ public class FractionTest extends TestCase {
try {
f = Fraction.getFraction("2 3");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("a 3");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("2 b/4");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("2 ");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction(" 3");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction(" ");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
}
@ -500,18 +546,22 @@ public class FractionTest extends TestCase {
try {
f = Fraction.getFraction("2/d");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("2e/3");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("2/");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
try {
f = Fraction.getFraction("/");
fail("expecting NumberFomatException");
} catch (NumberFormatException ex) {}
}
@ -625,6 +675,12 @@ public class FractionTest extends TestCase {
f = f.pow(-2);
assertEquals(25, f.getNumerator());
assertEquals(9, f.getDenominator());
f = Fraction.getFraction(Integer.MAX_VALUE);
try {
f = f.pow(2);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
}
public void testAdd() {
@ -656,12 +712,24 @@ public class FractionTest extends TestCase {
assertEquals(-1, f.getNumerator());
assertEquals(5, f.getDenominator());
f1 = Fraction.getFraction(Integer.MAX_VALUE - 1, 1);
f2 = Fraction.ONE;
f = f1.add(f2);
assertEquals(Integer.MAX_VALUE, f.getNumerator());
assertEquals(1, f.getDenominator());
f1 = Fraction.getFraction(3, 5);
f2 = Fraction.getFraction(1, 2);
f = f1.add(f2);
assertEquals(11, f.getNumerator());
assertEquals(10, f.getDenominator());
f1 = Fraction.getFraction(3, 8);
f2 = Fraction.getFraction(1, 6);
f = f1.add(f2);
assertEquals(13, f.getNumerator());
assertEquals(24, f.getDenominator());
f1 = Fraction.getFraction(0, 5);
f2 = Fraction.getFraction(1, 5);
f = f1.add(f2);
@ -671,7 +739,26 @@ public class FractionTest extends TestCase {
try {
f.add(null);
fail("expecting IllegalArgumentException");
} catch (IllegalArgumentException ex) {}
f1 = Fraction.getFraction(Integer.MAX_VALUE - 1, 1);
f2 = Fraction.ONE;
f = f1.add(f2);
assertEquals(Integer.MAX_VALUE, f.getNumerator());
assertEquals(1, f.getDenominator());
try {
f = f.add(Fraction.ONE); // should overflow
fail("expecting ArithmeticException but got: " + f.toString());
} catch (ArithmeticException ex) {}
try {
f= Fraction.getFraction(-Integer.MAX_VALUE, 1);
f = f.add(f);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
}
public void testSubtract() {
@ -728,7 +815,16 @@ public class FractionTest extends TestCase {
try {
f.subtract(null);
fail("expecting IllegalArgumentException");
} catch (IllegalArgumentException ex) {}
try {
f1 = Fraction.getFraction(1, Integer.MAX_VALUE);
f2 = Fraction.getFraction(1, Integer.MAX_VALUE - 1);
f = f1.subtract(f2);
fail("expecting ArithmeticException"); //should overflow
} catch (ArithmeticException ex) {}
}
public void testMultiply() {
@ -759,9 +855,28 @@ public class FractionTest extends TestCase {
f = f1.multiplyBy(f2);
assertSame(Fraction.ZERO, f);
f1 = Fraction.getFraction(2, 7);
f2 = Fraction.ONE;
f = f1.multiplyBy(f2);
assertEquals(2, f.getNumerator());
assertEquals(7, f.getDenominator());
try {
f.multiplyBy(null);
fail("expecting IllegalArgumentException");
} catch (IllegalArgumentException ex) {}
try {
f1 = Fraction.getFraction(1, Integer.MAX_VALUE);
f = f1.multiplyBy(f1); // should overflow
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f1 = Fraction.getFraction(1, -Integer.MAX_VALUE);
f = f1.multiplyBy(f1); // should overflow
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
}
public void testDivide() {
@ -779,6 +894,7 @@ public class FractionTest extends TestCase {
f2 = Fraction.ZERO;
try {
f = f1.divideBy(f2);
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
f1 = Fraction.getFraction(0, 5);
@ -786,9 +902,32 @@ public class FractionTest extends TestCase {
f = f1.divideBy(f2);
assertSame(Fraction.ZERO, f);
f1 = Fraction.getFraction(2, 7);
f2 = Fraction.ONE;
f = f1.divideBy(f2);
assertEquals(2, f.getNumerator());
assertEquals(7, f.getDenominator());
f1 = Fraction.getFraction(1, Integer.MAX_VALUE);
f = f1.divideBy(f1);
assertEquals(1, f.getNumerator());
assertEquals(1, f.getDenominator());
try {
f.divideBy(null);
fail("IllegalArgumentException");
} catch (IllegalArgumentException ex) {}
try {
f1 = Fraction.getFraction(1, Integer.MAX_VALUE);
f = f1.divideBy(f1.invert()); // should overflow
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
try {
f1 = Fraction.getFraction(1, -Integer.MAX_VALUE);
f = f1.divideBy(f1.invert()); // should overflow
fail("expecting ArithmeticException");
} catch (ArithmeticException ex) {}
}
public void testEquals() {
@ -834,10 +973,12 @@ public class FractionTest extends TestCase {
try {
f1.compareTo(null);
fail("expecting NullPointerException");
} catch (NullPointerException ex) {}
try {
f1.compareTo(new Object());
fail("expecting ClassCastException");
} catch (ClassCastException ex) {}
f2 = Fraction.getFraction(2, 5);