st.extreme.math.fraction.BigFraction Maven / Gradle / Ivy
Show all versions of fractions Show documentation
package st.extreme.math.fraction;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Immutable arbitrary precision fractions.
*
* The implementation is based on a {@link BigInteger} value for both numerator and denominator.
* All operations on fractions can be performed through {@link BigInteger} multiplication, addition and subtraction. And these have
* exact precision.
*
* Cancellation is always done on construction, using {@link BigInteger#gcd(BigInteger)}.
*
* Use case 1: division and multiplication with the same value
* This is the most common source of rounding problems.
*
*
* @Test
* public void testUseCase1() {
* BigFraction start = BigFraction.valueOf("1000");
* BigFraction divisor = BigFraction.valueOf("21");
* BigFraction quotient = start.divide(divisor);
* BigFraction result = quotient.multiply(divisor);
* assertEquals("1000", result.toString());
* assertEquals("1000", result.toFractionString());
* assertEquals(start, result);
* }
*
* @Test
* public void testUseCase1_BigDecimal() {
* BigDecimal start = new BigDecimal("1000");
* BigDecimal divisor = new BigDecimal("21");
* BigDecimal quotient = start.divide(divisor, new MathContext(30, RoundingMode.HALF_UP));
* BigDecimal result = quotient.multiply(divisor);
* assertEquals("1000", result.toPlainString());
* assertEquals(start, result);
* }
*
*
* While {@code testUseCase1()} passes, {@code testUseCase1_BigDecimal()} fails. It is very hard to find a {@link MathContext} so that the
* second test passes.
*
* Use case 2: calculation with fractions
*
*
* @Test
* public void testUseCase2_multiply() {
* BigFraction bf1 = BigFraction.valueOf("2/3");
* BigFraction bf2 = BigFraction.valueOf("-6/7");
* BigFraction result = bf1.multiply(bf2);
* assertEquals("-4/7", result.toFractionString());
* }
*
* @Test
* public void testUseCase2_divide() {
* BigFraction bf1 = BigFraction.valueOf("2/3");
* BigFraction bf2 = BigFraction.valueOf("6/7");
* BigFraction result = bf1.divide(bf2);
* assertEquals("7/9", result.toFractionString());
* }
*
* @Test
* public void testUseCase2_add() {
* BigFraction bf1 = BigFraction.valueOf("2/15");
* BigFraction bf2 = BigFraction.valueOf("6/5");
* BigFraction result = bf1.add(bf2);
* assertEquals("4/3", result.toFractionString());
* }
*
* @Test
* public void testUseCase2_subtract() {
* BigFraction bf1 = BigFraction.valueOf("8/15");
* BigFraction bf2 = BigFraction.valueOf("6/5");
* BigFraction result = bf1.subtract(bf2);
* assertEquals("-2/3", result.toFractionString());
* }
*
* @Test
* public void testUseCase2_pow() {
* BigFraction bf1 = BigFraction.valueOf("-2/3");
* BigFraction result = bf1.pow(-3);
* assertEquals("-27/8", result.toFractionString());
* }
*
*
* @author Otmar Humbel
*/
public final class BigFraction extends Number implements Comparable {
/**
* The {@link BigFraction} representing the value {@code 1}
*/
public static final BigFraction ONE = new BigFraction(BigInteger.ONE, BigInteger.ONE);
/**
* The {@link BigFraction} representing the value {@code 0}
*/
public static final BigFraction ZERO = new BigFraction(BigInteger.ZERO, BigInteger.ONE);
/**
* The pattern a decimal input String has to match
*/
static final Pattern DECIMAL_PATTERN = Pattern.compile("([-|+])?\\d+(\\.\\d+)?");
/**
* The pattern a fraction input String has to match
*/
static final Pattern FRACTION_PATTERN = Pattern.compile("([-|+])?\\d+/([-|+])?\\d+");
/**
* The default {@link MathContext} for conversions into {@link BigDecimal}.
*/
private static final MathContext DEFAULT_MATH_CONTEXT = new MathContext(500, RoundingMode.HALF_UP);
/**
* The serial version id
*/
private static final long serialVersionUID = 1295910738820044783L;
/**
* String constant for the digit {@code 1}
*/
private static final String STRING_ONE = "1";
/**
* String constant for the digit {@code 0}
*/
private static final String STRING_ZERO = "0";
/**
* A Set of String values qualifying as {@code zero} input
*/
private static final Set ZERO_VALUES;
static {
ZERO_VALUES = new HashSet<>();
ZERO_VALUES.add("");
ZERO_VALUES.add("0");
ZERO_VALUES.add("+0");
ZERO_VALUES.add("-0");
ZERO_VALUES.add("0.0");
ZERO_VALUES.add("+0.0");
ZERO_VALUES.add("-0.0");
ZERO_VALUES.add("0.");
ZERO_VALUES.add("+0.");
ZERO_VALUES.add("-0.");
ZERO_VALUES.add(".0");
ZERO_VALUES.add("+.0");
ZERO_VALUES.add("-.0");
}
/**
* The numerator
*
* For easier calculation, the sign of the fraction is kept in the numerator.
*/
private final BigInteger numerator;
/**
* The denominator
*
* For easier calculation, the denominator is always kept positive (without a sign).
*/
private final BigInteger denominator;
/**
* Create a {@link BigFraction} from a {@link String} numerator and denominator.
*
* Both values have to be accepted by {@link BigInteger#BigInteger(String)}.
*
* Because the denominator is always kept positive, it is possible that the signs of both numerator and denominator are inverted on
* construction.
*
* A {@link BigFraction} is always cancelled on construction, for example:
* {@code 4/6} will be cancelled into {@code 2/3}.
*
* @param numerator The numerator
* @param denominator The denominator
*/
public BigFraction(String numerator, String denominator) {
this(new BigInteger(numerator), new BigInteger(denominator));
}
/**
* Create a {@link BigFraction} from a {@link BigInteger} numerator and denominator.
*
* Because the denominator is always kept positive, it is possible that the signs of both numerator and denominator are inverted on
* construction.
*
* A {@link BigFraction} is always cancelled on construction, for example:
* {@code 4/6} will be cancelled into {@code 2/3}.
*
* @param numerator The numerator
* @param denominator The denominator
*/
public BigFraction(BigInteger numerator, BigInteger denominator) {
if (BigInteger.ZERO.equals(denominator)) {
throw new ArithmeticException("division by zero is not allowed.");
}
// always keep the denominator positive
if (denominator.signum() < 0) {
numerator = numerator.negate();
denominator = denominator.negate();
}
// always cancel if necessary
BigInteger gcd = numerator.gcd(denominator);
if (gcd.compareTo(BigInteger.ONE) > 0) {
numerator = numerator.divide(gcd);
denominator = denominator.divide(gcd);
}
this.numerator = numerator;
this.denominator = denominator;
}
/**
* Convert this {@code BigFraction} into an {@code int} value.
*
* @see BigInteger#intValue()
*/
@Override
public int intValue() {
return numerator.divide(denominator).intValue();
}
/**
* Convert this {@code BigFraction} into a {@code long} value.
*
* @see BigInteger#longValue()
*/
@Override
public long longValue() {
return numerator.divide(denominator).longValue();
}
/**
* Convert this {@code BigFraction} into a {@code float} value.
*
* @see BigDecimal#floatValue()
*/
@Override
public float floatValue() {
return bigDecimalValue().floatValue();
}
/**
* Convert this {@code BigFraction} into a {@code double} value.
*
* @see BigDecimal#doubleValue()
*/
@Override
public double doubleValue() {
return bigDecimalValue().doubleValue();
}
/**
* Compare this {@code BigFraction} with the specified {@link Number}.
*
* @param number {@link Number} to which this {@code BigFraction} is to be compared.
* @return {@code -1}, {@code 0} or {@code 1} as this {@code BigFraction} is numerically less than, equal to, or greater than
* {@code number}.
*/
public int compareToNumber(Number number) {
if (number == null) {
throw new NullPointerException("Comparison to a null value is not possible, see java.lang.Comparable");
}
final BigFraction other;
if (number instanceof BigFraction) {
other = (BigFraction) number;
} else {
other = BigFraction.valueOf(number);
}
return compareTo(other);
}
/**
* Compare this {@code BigFraction} with the specified {@link BigFraction}.
*
* To determine if this {@code BigFraction} is numerically equal to a {@link Number}, use {@link #compareToNumber(Number)}.
*
* @param other {@code BigFraction} to which this {@code BigFraction} is to be compared.
* @return {@code -1}, {@code 0} or {@code 1} as this {@code BigFraction} is numerically less than, equal to, or greater
* than{@code number}.
*
* @see BigFraction#compareToNumber(Number)
*/
@Override
public int compareTo(BigFraction other) {
if (other == this) {
return 0;
}
if (other == null) {
throw new NullPointerException("Comparison to a null value is not possible, see java.lang.Comparable");
}
if (denominator.equals(other.denominator)) {
return numerator.compareTo(other.numerator);
} else {
return numerator.multiply(other.denominator).compareTo(other.numerator.multiply(denominator));
}
}
/**
* Compare this {@code BigFraction} with the specified {@link Object} for equality.
* Equality can only be reached by {@code object} being another {@link BigFraction}.
*
* To determine if this {@code BigFraction} is numerically equal to a {@code BigFraction}, use {@link #compareTo(BigFraction)}.
*
* To determine if this {@code BigFraction} is numerically equal to a {@link Number}, use {@link #compareToNumber(Number)}.
*
* @param object {@link Object} to which this {@code BigFraction} is to be compared.
*
* @return {@code true} if and only if the specified Object is a {@link BigFraction} whose {@code numerator} and {@code denominator} are
* equal to this {@code BigFraction}'s.
*
* @see BigFraction#compareTo(BigFraction)
* @see BigFraction#compareToNumber(Number)
*/
@Override
public boolean equals(Object object) {
if (object instanceof BigFraction) {
if (object == this) {
return true;
}
BigFraction other = (BigFraction) object;
return Objects.equals(denominator, other.denominator) && Objects.equals(numerator, other.numerator);
} else {
return false;
}
}
/**
* Calculate the hash code for this {@code BigFraction}.
*
* @return the hash code for this {@code BigFraction}.
*/
@Override
public int hashCode() {
int hash = 17;
hash = 31 * hash + Objects.hashCode(denominator);
hash = 31 * hash + Objects.hashCode(numerator);
return hash;
}
/**
* Create a {@link BigFraction} with the reciprocal value of this {@code BigFraction}.
*
* @return a new {@code BigFraction} with the reciprocal value of this {@code BigFraction}.
*/
public BigFraction reciprocal() {
return new BigFraction(denominator, numerator);
}
/**
* Return the numerator of this {@code BigFraction}.
*
* @return the numerator
*/
public BigInteger getNumerator() {
return numerator;
}
/**
* Return the denominator of this {@code BigFraction}.
*
* @return the denominator
*/
public BigInteger getDenominator() {
return denominator;
}
/**
* Return the {@code signum} function of this {@code BigFraction}.
*
* @return {@code -1}, {@code 0} or {@code 1} as the value of this {@code BigFraction} is negative, zero, or positive.
*/
public int signum() {
return numerator.signum();
}
/**
* Return a human readable fractional representation of this {@code BigFraction}, such as {@code -2/3}.
* This representation can always be parsed exactly by {@link #valueOf(String)}.
*
* @return a fractional representation of this {@code BigFraction}.
*/
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(numerator.toString());
if (BigInteger.ONE.compareTo(denominator) != 0) {
builder.append('/');
builder.append(denominator.toString());
}
return builder.toString();
}
/**
* Return a human readable numerical representation of this {@code BigFraction}, such as {@code -1.75}.
* This representation might not always be able to be parsed exactly by {@link #valueOf(String)}.
*
* @see #bigDecimalValue()
* @see BigDecimal#toPlainString()
*
* @return a numerical representation of this {@code BigFraction}.
*/
public String toPlainString() {
return bigDecimalValue().toPlainString();
}
/**
* Create a new {@code BigFraction} from an {@code int} input
*
* @param i an {@code int} value.
* @return a {@code BigFraction} instance representing {@code i}.
*/
public static BigFraction valueOf(int i) {
return new BigFraction(BigInteger.valueOf(i), BigInteger.ONE);
}
/**
* Create a new {@code BigFraction} from a {@code long} input
*
* @param l a {@code long} value.
* @return a {@code BigFraction} instance representing {@code l}.
*/
public static BigFraction valueOf(long l) {
return new BigFraction(BigInteger.valueOf(l), BigInteger.ONE);
}
/**
* Create a new {@code BigFraction} from a {@code double} input
*
* @param d a {@code double} value.
* @return a {@code BigFraction} instance representing {@code d}.
*/
public static BigFraction valueOf(double d) {
return valueOf(Double.valueOf(d));
}
/**
* Create a new {@code BigFraction} from a {@code float} input
*
* @param f a {@code float} value.
* @return a {@code BigFraction} instance representing {@code f}.
*/
public static BigFraction valueOf(float f) {
return valueOf(Float.valueOf(f));
}
/**
* Create a new {@code BigFraction} from a {@link Number} input.
*
* @param number a {@link Number} value.
* @return a {@code BigFraction} instance representing {@code number}.
*/
public static BigFraction valueOf(Number number) {
if (number instanceof BigDecimal) {
return valueOf(((BigDecimal) number).toPlainString());
}
if (number instanceof Integer || number instanceof Long) {
return new BigFraction(BigInteger.valueOf(number.longValue()), BigInteger.ONE);
}
if (number instanceof BigFraction) {
BigFraction other = (BigFraction) number;
return new BigFraction(other.numerator, other.denominator);
}
return valueOf(number.toString());
}
/**
* Create a new {@code BigFraction} from a {@link String} input.
*
* The input can have the following formats:
*
* - {@code 123}
*
- {@code -123}
*
- {@code -123.678}
*
- {@code 2/3}
*
- {@code -2/3}
*
- {@code 2/-3}
*
- {@code -2/-3}
*
*
* @param numberString in one of the formats described above
*
* @return a new {@code BigFraction} representing the passed in {@code numberString}.
*
* @throws NumberFormatException if the input does not represent a valid fraction
*/
public static BigFraction valueOf(String numberString) {
if (isZeroStringInput(numberString)) {
return ZERO;
}
boolean matchesDecimal = DECIMAL_PATTERN.matcher(numberString).matches();
final boolean matchesFraction;
if (matchesDecimal) {
matchesFraction = false;
} else {
matchesFraction = FRACTION_PATTERN.matcher(numberString).matches();
}
if (!matchesDecimal && !matchesFraction) {
try {
numberString = new BigDecimal(numberString).toPlainString();
matchesDecimal = true;
} catch (NumberFormatException nfe) {
throw new NumberFormatException(buildNumberFormatExceptionMessage(numberString));
}
}
if (matchesDecimal) {
return valueOfDecimalString(numberString);
} else {
return valueOfFractionString(numberString);
}
}
/**
* Convert this {@code BigFraction } into a {@link BigDecimal} value, using the {@link BigFraction#DEFAULT_MATH_CONTEXT}.
* The default {@link MathContext} uses precision {@code 500} and {@link RoundingMode#HALF_UP}
*
* This method is intended to round the final result into a {@link BigDecimal} .
*
* @return a maybe not exact representation of this {@code BigFraction} as a {@link BigDecimal} value.
*/
public BigDecimal bigDecimalValue() {
return bigDecimalValue(DEFAULT_MATH_CONTEXT);
}
/**
* Convert this {@code BigFraction } into a {@link BigDecimal} value, using the given {@link MathContext}.
*
* This method is intended to round the final result into a {@link BigDecimal} .
*
* @param mathContext The desired target {@link MathContext}
*
* @return a maybe not exact representation of this {@code BigFraction} as a {@link BigDecimal} value.
*/
public BigDecimal bigDecimalValue(MathContext mathContext) {
return new BigDecimal(numerator).divide(new BigDecimal(denominator), mathContext);
}
/**
* Multiply this {@code BigFraction} by another {@link BigFraction} value.
*
* @param value The value this {@code BigFraction} is to be multiplied with.
* @return a new {@code BigFraction} representing the product of this {@code BigFraction} and {@code value}.
*/
public BigFraction multiply(BigFraction value) {
boolean cancelUpperLeftLowerRight = false;
boolean cancelLowerLeftUpperRight = false;
if (numerator.equals(value.denominator)) {
cancelUpperLeftLowerRight = true;
}
if (denominator.equals(value.numerator)) {
cancelLowerLeftUpperRight = true;
}
if (cancelUpperLeftLowerRight && cancelLowerLeftUpperRight) {
return ONE;
} else if (cancelUpperLeftLowerRight) {
return new BigFraction(value.numerator, denominator);
} else if (cancelLowerLeftUpperRight) {
return new BigFraction(numerator, value.denominator);
} else {
return new BigFraction(numerator.multiply(value.numerator), denominator.multiply(value.denominator));
}
}
/**
* Divide this {@code BigFraction} by another {@link BigFraction} value.
*
* @param value The value this {@code BigFraction} is to be divided with.
* @return a new {@code BigFraction} representing the quotient of this {@code BigFraction} and {@code value}.
*/
public BigFraction divide(BigFraction value) {
return multiply(value.reciprocal());
}
/**
* Add this {@code BigFraction} and another {@link BigFraction} value.
*
* @param value The value this {@code BigFraction} is to be added to.
* @return a new {@code BigFraction} representing the sum of this {@code BigFraction} and {@code value}.
*/
public BigFraction add(BigFraction value) {
if (denominator.equals(value.denominator)) {
return new BigFraction(numerator.add(value.numerator), denominator);
}
return addOrSubtract(value, true);
}
/**
* Subtract another {@link BigFraction} value from this {@code BigFraction}.
*
* @param value The value to be subtracted from this {@code BigFraction}.
* @return a new {@code BigFraction} representing this {@code BigFraction} minus {@code value}.
*/
public BigFraction subtract(BigFraction value) {
if (denominator.equals(value.denominator)) {
return new BigFraction(numerator.subtract(value.numerator), denominator);
}
return addOrSubtract(value, false);
}
/**
* Negate this {@code BigFraction}.
*
* @return a new {@code BigFraction} representing the product of this {@code BigFraction} and {@code -1}.
*/
public BigFraction negate() {
return new BigFraction(numerator.negate(), denominator);
}
/**
* Calculate the absolute value of this {@code BigFraction}.
*
* @return a new {@code BigFraction} representing the absolute value of this {@code BigFraction}.
*/
public BigFraction abs() {
return new BigFraction(numerator.abs(), denominator);
}
/**
* Calculate the power of this {@code BigFraction} by an {@code int} exponent.
*
* @param exponent The exponent. Can be any {@code int}, including negative values.
* @return a new {@code BigFraction} representing {@code this}{@code exponent}.
*/
public BigFraction pow(int exponent) {
if (exponent == 0) {
return ONE;
}
if (BigInteger.ZERO.equals(numerator)) {
return ZERO;
}
if (exponent < 0) {
return reciprocal().pow(-exponent);
}
return new BigFraction(numerator.pow(exponent), denominator.pow(exponent));
}
/**
* Build the message for a {@link NumberFormatException}.
*
* @param numberString
* @return the message
*/
private static String buildNumberFormatExceptionMessage(String numberString) {
return "illegal number format '".concat(numberString).concat("'.");
}
/**
* Determine if the input qualifies as {@link BigFraction#ZERO}.
*
* @param numberString
* @return {@code true} if the string is evaluated to zero, {@code false} otherwise.
*/
private static boolean isZeroStringInput(String numberString) {
if (numberString == null || ZERO_VALUES.contains(numberString)) {
return true;
}
return false;
}
/**
* @param decimalString The caller has to make sure that {@code decimalString} matches {@link DECIMAL_PATTERN}
* @return a new {@code BigFraction} with the value of {@code decimalString}
*/
private static BigFraction valueOfDecimalString(String decimalString) {
String[] values = decimalString.split("\\.");
String integerPart = values[0];
if (values.length == 1) {
return new BigFraction(new BigInteger(integerPart), BigInteger.ONE);
} else {
String decimalPart = values[1];
StringBuilder numerator = new StringBuilder(integerPart);
numerator.append(decimalPart);
StringBuilder denominator = new StringBuilder();
denominator.append(STRING_ONE);
int decimals = decimalPart.length();
for (int i = 0; i < decimals; i++) {
denominator.append(STRING_ZERO);
}
return new BigFraction(numerator.toString(), denominator.toString());
}
}
/**
* @param fractionString The caller has to make sure that {@code fractionString} matches {@link FRACTION_PATTERN}
* @return a new {@code BigFraction} with the value of {@code fractionString}
*/
private static BigFraction valueOfFractionString(String fractionString) {
String[] values = fractionString.split("/");
return new BigFraction(values[0], values[1]);
}
/**
* Internal helper method to perform either an addition or a subtraction.
*
* Uses lcm as common denominator.
*
* Keeps the values as small as possible, and tries to minimize the number of {@code BigInteger} operations.
*
* @param value The {@link BigFraction} added to (or subtracted from) this {@link BigFraction}
* @param add if {@code true} an addition is performed, otherwise a subtraction
* @return a new {@code BigFraction} representing the result
*/
private BigFraction addOrSubtract(BigFraction value, boolean add) {
BigInteger gcd = denominator.gcd(value.denominator); // both denominators are positive and non-zero
final BigInteger expansion;
final BigInteger valueExpansion;
if (BigInteger.ONE.equals(gcd)) {
expansion = value.denominator;
valueExpansion = denominator;
} else {
expansion = value.denominator.divide(gcd);
valueExpansion = denominator.divide(gcd);
}
final BigInteger resultNumerator;
if (add) {
resultNumerator = expansion.multiply(numerator).add(valueExpansion.multiply(value.numerator));
} else {
resultNumerator = expansion.multiply(numerator).subtract(valueExpansion.multiply(value.numerator));
}
return new BigFraction(resultNumerator, expansion.multiply(denominator));
}
}