(See the previous iteration.)
I have improved my Fraction
implementation taking the answers in the previous iteration into account.
Differences
- Both numerator and denominator are now instances of
BigInteger
. - The class also caches a
BigDecimal
representing the value of the ratio with no more thanscale
decimals. - When constructing a
Fraction
, both the numerator and denominator are divided by their greatest common divisor. This is much shorter and efficient than in the previous iteration, where I dealt with prime factorization to do the same task. Fraction
is declaredfinal
.toString()
is simplified.- The check whether the new
Fraction
is positive or negative is simplified. serialVersionUID
is added.- Added two public constants for zero and one.
However, I did not like the static factory pattern, so it's not here.
See what I have:
Fraction.java:
package net.coderodde.math;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Objects;
/**
* This class implements a fraction consisting of a numerator and a denominator.
*/
public final class Fraction extends Number {
public static final Fraction ZERO = new Fraction(0, 1);
public static final Fraction ONE = new Fraction(1, 1);
private static final int DEFAULT_SCALE = 5;
private static final long serialVersionUID = 185726081998L;
private final BigInteger numerator;
private final BigInteger denominator;
private final BigDecimal value;
private Fraction(BigInteger numerator,
BigInteger denominator,
final int scale) {
Objects.requireNonNull(numerator, "The input numerator is null.");
Objects.requireNonNull(denominator, "The input denominator is null.");
if (denominator.equals(BigInteger.ZERO)) {
throw new IllegalArgumentException("The denominator is zero.");
}
if (numerator.equals(BigInteger.ZERO)) {
this.numerator = BigInteger.ZERO;
this.denominator = BigInteger.ONE;
this.value = BigDecimal.ZERO;
} else {
final boolean isPositive =
numerator .compareTo(BigInteger.ZERO) > 0 ==
denominator.compareTo(BigInteger.ZERO) > 0;
numerator = numerator.abs();
denominator = denominator.abs();
final BigInteger greatestCommonDivisor = gcd(numerator,
denominator);
numerator = numerator .divide(greatestCommonDivisor);
denominator = denominator.divide(greatestCommonDivisor);
this.numerator = isPositive ? numerator : numerator.negate();
this.denominator = denominator;
this.value = new BigDecimal(this.numerator)
.divide(new BigDecimal(this.denominator),
scale,
RoundingMode.HALF_EVEN);
}
}
public Fraction(final BigInteger numerator, final BigInteger denominator) {
this(numerator, denominator, DEFAULT_SCALE);
}
public Fraction(final long numerator,
final long denominator,
final int scale) {
this(BigInteger.valueOf(numerator),
BigInteger.valueOf(denominator),
scale);
}
public Fraction(final long numerator, final long denominator) {
this(numerator, denominator, DEFAULT_SCALE);
}
public Fraction plus(final Fraction other) {
return new Fraction(
this.numerator.multiply(other.denominator)
.add(this.denominator.multiply(other.numerator)),
this.denominator.multiply(other.denominator));
}
public Fraction minus(final Fraction other) {
return new Fraction(
this.numerator.multiply(other.denominator)
.subtract(this.denominator
.multiply(other.numerator)),
this.denominator.multiply(other.denominator));
}
public Fraction multiply(final Fraction other) {
return new Fraction(this.numerator .multiply(other.numerator),
this.denominator.multiply(other.denominator));
}
public Fraction divide(final Fraction other) {
return new Fraction(this.numerator .multiply(other.denominator),
this.denominator.multiply(other.numerator));
}
public Fraction abs() {
return new Fraction(numerator.abs(), denominator);
}
public Fraction neg() {
return new Fraction(numerator.negate(), denominator);
}
public BigInteger getNumerator() {
return numerator;
}
public BigInteger getDenominator() {
return denominator;
}
public BigDecimal getValue() {
return value;
}
@Override
public String toString() {
return numerator + "/" + denominator;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (!getClass().equals(o.getClass())) {
return false;
}
final Fraction other = (Fraction) o;
return numerator.equals(other.numerator)
&& denominator.equals(other.denominator);
}
@Override
public int hashCode() {
return Objects.hash(numerator, denominator);
}
@Override
public int intValue() {
return value.intValue();
}
@Override
public long longValue() {
return value.longValue();
}
@Override
public float floatValue() {
return value.floatValue();
}
@Override
public double doubleValue() {
return value.doubleValue();
}
private BigInteger gcd(final BigInteger a, final BigInteger b) {
return b.equals(BigInteger.ZERO) ? a : gcd(b, a.mod(b));
}
public static void main(String[] args) {
System.out.println(new Fraction(1, 6, 4).getValue());
}
}
FractionTest.java:
package net.coderodde.math;
import java.math.BigInteger;
import org.junit.Test;
import static org.junit.Assert.*;
public class FractionTest {
private static float DELTA = 0.001f;
@Test
public void testConstruct() {
assertEquals(new Fraction(5, 3) , new Fraction(35, 21));
assertEquals(new Fraction(5, 3) , new Fraction(-35, -21));
assertEquals(new Fraction(-5, 3), new Fraction(-35, 21));
assertEquals(new Fraction(-5, 3), new Fraction(35, -21));
assertEquals(new Fraction(0, 1) , new Fraction(0, 100));
}
@Test(expected = IllegalArgumentException.class)
public void testThrowsOnZeroDenominator() {
new Fraction(1, 0);
}
@Test
public void testPlus() {
// (7 / 3) + (6 / 5) = (35 / 15) + (18 / 15) = 53 / 15
Fraction a = new Fraction(7, 3);
Fraction b = new Fraction(6, 5);
assertEquals(new Fraction(53, 15), a.plus(b));
assertEquals(new Fraction(53, 15), b.plus(a));
a = new Fraction(-7, 3);
b = new Fraction(6, -5);
assertEquals(new Fraction(-53, 15), a.plus(b));
a = new Fraction(7, 3);
b = new Fraction(6, -5);
// (7 / 3) - (6 / 5) = (35 / 15) - (18 / 15) = 17 / 15
assertEquals(new Fraction(17, 15), a.plus(b));
}
@Test
public void testMinus() {
// (7 / 3) - (6 / 5) = (35 / 15) - (18 / 15) = 17 / 15
Fraction a = new Fraction(7, 3);
Fraction b = new Fraction(6, 5);
assertEquals(new Fraction(17, 15), a.minus(b));
assertEquals(new Fraction(17, -15), b.minus(a));
assertEquals(new Fraction(-17, 15), b.minus(a));
}
@Test
public void testMultiply() {
Fraction a = new Fraction(3, 7);
Fraction b = new Fraction(5, 3);
assertEquals(new Fraction(5, 7), a.multiply(b));
b = new Fraction(-5, 3);
assertEquals(new Fraction(-5, 7), a.multiply(b));
assertEquals(new Fraction(5, -7), a.multiply(b));
}
@Test
public void testDivide() {
// (2/9) / (6/4) = (2/9) * (2/3) = 4 / 27
Fraction a = new Fraction(2, 9);
Fraction b = new Fraction(6, 4);
assertEquals(new Fraction(4, 27), a.divide(b));
assertEquals(new Fraction(-4, -27), a.divide(b));
}
@Test
public void testAbs() {
assertEquals(new Fraction(2, 4), new Fraction( 1, 2).abs());
assertEquals(new Fraction(2, 4), new Fraction(-1, 2).abs());
assertEquals(new Fraction(2, 4), new Fraction( 1, -2).abs());
assertEquals(new Fraction(2, 4), new Fraction(-1, -2).abs());
}
@Test
public void testGetNumerator() {
assertEquals(BigInteger.valueOf(3), new Fraction(6, 4) .getNumerator());
assertEquals(BigInteger.valueOf(3), new Fraction(3, 2) .getNumerator());
assertEquals(BigInteger.valueOf(3), new Fraction(9, 6) .getNumerator());
assertEquals(BigInteger.valueOf(15), new Fraction(15, 11).getNumerator());
}
@Test
public void testGetDenominator() {
assertEquals(BigInteger.valueOf(2), new Fraction(6, 4) .getDenominator());
assertEquals(BigInteger.valueOf(2), new Fraction(3, 2) .getDenominator());
assertEquals(BigInteger.valueOf(2), new Fraction(9, 6) .getDenominator());
assertEquals(BigInteger.valueOf(11), new Fraction(15, 11).getDenominator());
}
@Test
public void testToString() {
assertEquals("3/2" , new Fraction(6 , 4) .toString());
assertEquals("3/2" , new Fraction(3 , 2) .toString());
assertEquals("3/2" , new Fraction(9 , 6) .toString());
assertEquals("15/11" , new Fraction(15 , 11) .toString());
assertEquals("-15/11", new Fraction(-15, 11) .toString());
assertEquals("-15/11", new Fraction(15 , -11).toString());
assertEquals("15/11" , new Fraction(-15, -11).toString());
assertEquals("0/1" , new Fraction(0, -123) .toString());
}
@Test
public void testIntValue() {
assertEquals(0, new Fraction(0, 4).intValue());
assertEquals(0, new Fraction(1, 4).intValue());
assertEquals(0, new Fraction(2, 4).intValue());
assertEquals(0, new Fraction(3, 4).intValue());
assertEquals(1, new Fraction(4, 4).intValue());
assertEquals(1, new Fraction(5, 4).intValue());
assertEquals(1, new Fraction(6, 4).intValue());
assertEquals(1, new Fraction(7, 4).intValue());
assertEquals(0, new Fraction(-0, 4).intValue());
assertEquals(0, new Fraction(-1, 4).intValue());
assertEquals(0, new Fraction(-2, 4).intValue());
assertEquals(0, new Fraction(-3, 4).intValue());
assertEquals(-1, new Fraction(-4, 4).intValue());
assertEquals(-1, new Fraction(-5, 4).intValue());
assertEquals(-1, new Fraction(-6, 4).intValue());
assertEquals(-1, new Fraction(-7, 4).intValue());
assertEquals(4, new Fraction(-17, -4).intValue());
}
@Test
public void testLongValue() {
assertEquals(0L, new Fraction(0, 4).longValue());
assertEquals(0L, new Fraction(1, 4).longValue());
assertEquals(0L, new Fraction(2, 4).longValue());
assertEquals(0L, new Fraction(3, 4).longValue());
assertEquals(1L, new Fraction(4, 4).longValue());
assertEquals(1L, new Fraction(5, 4).longValue());
assertEquals(1L, new Fraction(6, 4).longValue());
assertEquals(1L, new Fraction(7, 4).longValue());
assertEquals(0L, new Fraction(-0, 4).longValue());
assertEquals(0L, new Fraction(-1, 4).longValue());
assertEquals(0L, new Fraction(-2, 4).longValue());
assertEquals(0L, new Fraction(-3, 4).longValue());
assertEquals(-1L, new Fraction(-4, 4).longValue());
assertEquals(-1L, new Fraction(-5, 4).longValue());
assertEquals(-1L, new Fraction(-6, 4).longValue());
assertEquals(-1L, new Fraction(-7, 4).longValue());
assertEquals(4L, new Fraction(-17, -4).longValue());
}
@Test
public void testFloatValue() {
assertEquals(0.0f , new Fraction(0, 4).floatValue(), DELTA);
assertEquals(0.25f, new Fraction(1, 4).floatValue(), DELTA);
assertEquals(0.5f , new Fraction(2, 4).floatValue(), DELTA);
assertEquals(0.75f, new Fraction(3, 4).floatValue(), DELTA);
assertEquals(1.0f , new Fraction(4, 4).floatValue(), DELTA);
assertEquals(1.25f, new Fraction(5, 4).floatValue(), DELTA);
assertEquals(1.5f , new Fraction(6, 4).floatValue(), DELTA);
assertEquals(1.75f, new Fraction(7, 4).floatValue(), DELTA);
assertEquals(0.0f , new Fraction(-0, 4).floatValue(), DELTA);
assertEquals(-0.25f, new Fraction(-1, 4).floatValue(), DELTA);
assertEquals(-0.5f , new Fraction(-2, 4).floatValue(), DELTA);
assertEquals(-0.75f, new Fraction(-3, 4).floatValue(), DELTA);
assertEquals(-1.0f , new Fraction(-4, 4).floatValue(), DELTA);
assertEquals(-1.25f, new Fraction(-5, 4).floatValue(), DELTA);
assertEquals(-1.5f , new Fraction(-6, 4).floatValue(), DELTA);
assertEquals(-1.75f, new Fraction(-7, 4).floatValue(), DELTA);
assertEquals(4.25f, new Fraction(-17, -4).floatValue(), DELTA);
}
@Test
public void testDoubleValue() {
assertEquals(0.0 , new Fraction(0, 4).doubleValue(), DELTA);
assertEquals(0.25, new Fraction(1, 4).doubleValue(), DELTA);
assertEquals(0.5 , new Fraction(2, 4).doubleValue(), DELTA);
assertEquals(0.75, new Fraction(3, 4).doubleValue(), DELTA);
assertEquals(1.0 , new Fraction(4, 4).doubleValue(), DELTA);
assertEquals(1.25, new Fraction(5, 4).doubleValue(), DELTA);
assertEquals(1.5 , new Fraction(6, 4).doubleValue(), DELTA);
assertEquals(1.75, new Fraction(7, 4).doubleValue(), DELTA);
assertEquals(0.0 , new Fraction(-0, 4).doubleValue(), DELTA);
assertEquals(-0.25, new Fraction(-1, 4).doubleValue(), DELTA);
assertEquals(-0.5 , new Fraction(-2, 4).doubleValue(), DELTA);
assertEquals(-0.75, new Fraction(-3, 4).doubleValue(), DELTA);
assertEquals(-1.0 , new Fraction(-4, 4).doubleValue(), DELTA);
assertEquals(-1.25, new Fraction(-5, 4).doubleValue(), DELTA);
assertEquals(-1.5 , new Fraction(-6, 4).doubleValue(), DELTA);
assertEquals(-1.75, new Fraction(-7, 4).doubleValue(), DELTA);
assertEquals(4.25, new Fraction(-17, -4).doubleValue(), DELTA);
}
}
Any critique much appreciated.