Rational calculation: add, subtract, multiply, divide and pow(int) with exact precision in Java.
The handling of BigDecimal is a bit cumbersome. The programmer needs to specify the precision (or MathContext) for almost every division, and - depending on the call chain - for the other arithmetic operations as well.
My goal is to provide an easy to use class which frees the user of taking care about precision.
The only point where the programmer has to make some assumptions about precision should be at the end of all the calculations, when she needs to convert the final result back into a custom value.
Every decimal value in the form 12345.6789 can be represented as a rational number, also called fraction. In this example, it would be 123456789 / 10000.
Arithmetics with fractions can be reduced to integer multiplications, integer additions and integer subtractions. And those can be performed with exact precision.
<dependency>
<groupId>st.extreme</groupId>
<artifactId>fractions</artifactId>
<version>1.1</version>
</dependency>Please adapt to your build system of choice. For more information, see Releases.
Immutable arbitrary precision fractions.
The implementation is based on a BigInteger value for both numerator and denominator.
All operations on fractions can be performed through BigInteger multiplication, addition and subtraction. And these have exact precision.
Cancellation is done on construction, using BigInteger.gcd(BigInteger).
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.toPlainString());
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 testUseCase1() passes, testUseCase1_BigDecimal() fails. It is very hard to find a MathContext so that the second test passes.
@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.toString());
}
@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.toString());
}
@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.toString());
}
@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.toString());
}
@Test
public void testUseCase2_pow() {
BigFraction bf1 = BigFraction.valueOf("-2/3");
BigFraction result = bf1.pow(-3);
assertEquals("-27/8", result.toString());
}- The implementation of
toString()andtoPlainString()have switched. This reflects the benaviour ofBigDecimal.toString()andBigDecimal.toPlainString(). - The
.valueOf(String)parsing of input can now handle more number-alike strings, especiallyBigDecimal.toEngineeringString()andBigDecimal.toString()with scientific notation. - Numerator and denominator now keep their signs. This will be reverted with the next release.
- Running this library now requires Java 8.
- The
MANIFEST.MFfile now has an attributeAutomatic-Module-Namewith the valuest.extreme.math.fraction, in order to ease the transition to Java 9. - Due to the automatic module name, the package for
BigFractionis nowst.extreme.math.fraction, instead ofst.extreme.mathbefore. equals()is now properly implemented and verified by EqualsVerifier. As a consequence, theBigFractionclass is nowfinal.- The
compareTo()method now only acceptsBigFractionany more (everything else would violate theComparablecontract). Thanks to cancellation, the natural ordering ofBigFractionis now consistent withequals(). - The new
compareToNumber()method allows comparisons tojava.lang.Numbertypes. - The
denominatornow is always kept positive (again). Some optimizations required this, so it is likely to stay this way.
The following people gave very valuable advice - many thanks to them: