A Rational Number in Java

Bill Seymour
2024-12-25


Contents


Introduction

This paper describes the public API of an open-source rational number in Java distributed under the Boost Software License.  (This is not part of Boost.  The author just likes their open-source license.)

It’s not actually possible to write a user-defined numeric type in Java due to absence of operator overloading; but you can fake it by writing a class that has methods with names like “add” and “subtract”; and that’s what the Rational class does.

Also included are a few methods that mimic java.lang.Math methods that return exact results, a getParts() method that decomposes this into its integral and fractional parts, and two methods that approximate square roots.

Interacting with floating point types should probably be done only sparingly since the whole point of doing rational arithmetic is doing it exactly, and floating point types are inexact in general; but if you need to do it, you probably really need to do it; so it’s possible to construct a Rational from a finite double or a BigDecimal; you can convert a Rational to a BigDecimal; and since the class claims to extend Number, it needs to override floatValue() and doubleValue() in any event.

Since almost every operation on rational numbers requires multiplication behind the scenes, numerators and denominators can get big in a hurry.  To mitigate the problem of overflow, the Rational class uses BigIntegers internally for its numerator and denominator.

Class invariants:

As of this writing, the code hasn’t even been compiled, let alone tested.  YMMV.

This paper, the (as yet untested) source code, and the Boost license are available here.


Efficiency concerns

Taking its cue from BigInteger and BigDecimal, the Rational class is immutable; and so if you do lots of arithmetic, you’ll be banging away at the heap like crazy because basically every arithmetic operation requires, not only a new Rational, but also numerous new BigIntegers.  Consider what it takes to figure the sum of two Rationals: I count four new BigIntegers just to do the computation, numerous new BigIntegers to reduce the fraction to its lowest terms (depends on how may iterations of Euclid’s algorithm it takes to find the greatest common divisor), and one new Rational to return the answer.  (Note that making Rational itself mutable wouldn’t help much because of the BigIntegers used internally, which is where the arithmetic actually happens.  Blame Java, don’t blame me.)

It might well speed things up by more than an order of nagnitude if we used longs instead of BigIntegers for the numerator and denominator; but then we’d run a very real risk of signed integer overflow.

Making the Rational class itself mutable and allowing numerators and denominators to grow without bound until the value becomes externally visible might help some because reducing the fraction to lowest terms isn’t done until constructing a new return value.


Synopsis

import java.math.BigInteger;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.math.MathContext;

package rat;

public final class Rational extends Number implements Comparable<Rational>
{
    //
    // Three constants:
    //

    public static final Rational ZERO;
    public static final Rational ONE;
    public static final Rational HALF;

    //
    // Constructors:
    //

    public Rational();

    public Rational(long value);
    public Rational(BigInteger value);

    public Rational(long numerator, long denominator);
    public Rational(BigInteger numerator, BigInteger denominator);

    public Rational(double value);
    public Rational(double value, double accuracy);
    public Rational(BigDecimal value);

    public Rational(String value);
    public Rational(String value, int radix);

    //
    // Observers:
    //

    @Override public int compareTo(Rational other);
    @Override public boolean equals(Object other);
    @Override public int hashCode();

    public BigInteger numerator();
    public BigInteger denominator();
    public int signum();
    public Rational[] getParts();

    //
    // Conversions to other types:
    //

    public String toString();
    public String toString(int radix);
    public String toString(boolean showDen1);
    public String toString(int radix, boolean showDen1);

    public BigInteger toBigInteger(RoundingMode rounding);
    public BigInteger toBigInteger();
    public BigInteger ceil();
    public BigInteger floor();
    public BigInteger rint();
    @Override public int intValue();
    @Override public long longValue();

    public BigDecimal toBigDecimal(MathContext rounding);
    public BigDecimal toBigDecimal();
    @Override public float floatValue();
    @Override public double doubleValue();

    //
    // Arithmetic operations:
    //

    public Rational negate();
    public Rational abs();
    public Rational reciprocal();

    public Rational min(Rational other);
    public Rational max(Rational other);
    public Rational copySign(Rational other);

    public Rational add(Rational addend);
    public Rational subtract(Rational subtrahend);
    public Rational multiply(Rational multiplier);
    public Rational divide(Rational divisor);

    public Rational round(RoundingMode rounding);

    public Rational IEEEremainder(Rational divisor);
    public Rational remainder(Rational divisor, RoundingMode rounding);

    public Rational pow(int exponent);
    public Rational sqr();

    public Rational sqrt();
    public Rational sqrt(double accuracy);
}


Detailed Descriptions


Three constants

public static final Rational ZERO;
public static final Rational ONE;
public static final Rational HALF;
The constant values, 0/1, 1/1, and 1/2.


Constructors

public Rational();
The default constructor constructs a Rational with a numerator equal to zero and a denominator equal to one.
public Rational(long value);
public Rational(BigInteger value);
These one-argument constructors construct a Rationsl with a numerator equal to value and a denominator equal to one.
public Rational(long numerator, long denominator);
public Rational(BigInteger numerator, BigInteger denominator);
These two-argument constructors construct a Rational with the specified numerator and denominator, and then normalize the fraction such that the denominator is greater than zero and the numerator and denominator have no common factor other than one.  If zero is passed as the denominator, these methods with throw an ArithmeticException to indicate division by zero.
public Rational(double value);
This constructs a Rational that’s exactly equal to value.  (But note that value is probably inexact to begin with, and this will likely generate really huge numerators and denominators in general.  This’ll be a good choice only if you know that you have a double value that’s some not-too-large power of two or if you really do need an extremely accurate conversion.)  This throws an IllegalArgumentException if value is non-finite.
public Rational(double value, double accuracy);
This constructs a Rational that’s approximately equal to value.  The second argument specifies the accuracy required.  For example, if ±0.1% of value is good enough, pass 0.001.  (The allowable error will be the absolute value of value * accuracy.)

This constructor uses the continued fractions algorithm; and it’ll get you much more reasonable numerators and denominators.  For example, Rational(Math.PI, 0.004) constructs 22/7 with just one time through the loop; but it might be slower if you request a very accurate conversion because it does lots of BigInteger arithmetic and so bangs away at the heap.

This method will throw an IllegalArgumentException if value is non-finite; and it will throw an ArithmeticException if the computed maximum error is non-finite or extremely small.  (At present, “extremely small” means subnormal.)

public Rational(BigDecimal value);
This constructs a Rational that’s exactly equal to value.  It makes a local copy of the argument with trailing zeros removed; then,
public Rational(String value);
public Rational(String value, int radix);
A Rational can also be constructed from a String.  If the radix is not specified, it defaults to 10.

In general, the string should contain only digits that are appropriate for the requested radix; but it may begin with a '+' or a '-'; and it may optionally contain a '/' or a '.' but not both.

In any event, the substrings before and after a '/', or the complete string after removing a '.', must make sense when passed to a BigInteger constructor.

If the substring after a '/' begins with a '-', the fraction will be normalized such that the denominator is greater than zero.  It’s not clear why H. sapiens would write such a thing; but it’s not hard to imaging a machine-generated string coming out like that.


Observers

@Override public int compareTo(Rational other);
@Override public boolean equals(Object other);
@Override public int hashCode();

public BigInteger numerator();
public BigInteger denominator();
public int signum();
These are all expected to be unsurprising.
public Rational[] getParts();
This method is inspired by the modf() function in the C standard library which breaks a floating-point value into its integral and fractional parts.  Rational.getParts() returns a Rational[2] with [0] being the integral part of this and [1] being the fractional part.


Conversions to other types

public String toString();
public String toString(int radix);
public String toString(boolean showDen1);
public String toString(int radix, boolean showDen1);
These methods return Strings in the general form, “numerator/denominator”.  If the radix is not specified, it defaults to 10.  If showDen1 is not specified, or if it’s false, and the denominator is 1, the “/1” will not be generated.
public BigInteger toBigInteger(RoundingMode rounding);
public BigInteger toBigInteger();
public BigInteger ceil();
public BigInteger floor();
public BigInteger rint();
@Override public int intValue();
@Override public long longValue();
These methods all round to an integer by calling toBigInteger(RoundingMode).  The no-argument toBigInteger() calls toBigInteger(RoundingMode.DOWN) which has the effect of truncating the value toward zero and is the correct behavior for intValue() and longValue().  As expected, the ceil(), floor() and rint() methods use the CEILING, FLOOR and HALF_EVEN modes respectively.

intValue() and longValue() quietly do narrowing conversions of the value returned by toBigInteger().

public BigDecimal toBigDecimal(MathContext rounding);
public BigDecimal toBigDecimal();
@Override public float floatValue();
@Override public double doubleValue();
Conversions to BigDecimals and floating point values will likely be inexact.

toBigDecimal(MathContext) first converts the numerator and denominator to BigDecimals with a scale of zero, then it divides the numerator by the denominator possibly rounding in accordance with the passed MathContext.

The no-argument toBigDecimal() defaults the rounding to MathContext.DECIMAL128; and floatValue() and doubleValue() do the expected narrowing conversions of that value.


Arithmetic operations

public Rational negate();
public Rational abs();
public Rational reciprocal();

public Rational min(Rational other);
public Rational max(Rational other);
public Rational copySign(Rational other);

public Rational add(Rational addend);
public Rational subtract(Rational subtrahend);
public Rational multiply(Rational multiplier);
public Rational divide(Rational divisor);
These are all expected to be unsurprising.  negate() will never create a negative zero; reciprocal() will throw an ArithmeticException if this is zero; and divide() will throw an ArithmeticException if divisor is zero.
public Rational round(RoundingMode rounding);
If this is not already an integer, this method throws an ArithmeticException if RoundingMode.UNNECESSARY is passed.  It can also throw an IllegalArgumentException if the argument is not one of the RoundingMode enumerators, but that seems unlikely.
public Rational IEEEremainder(Rational divisor);
public Rational remainder(Rational divisor, RoundingMode rounding);
IEEEremainder() returns the remainder of this divided by divisor as specified in the IEEE 754 floating point standard.

The two-argument method allows specifying the rounding mode to use after the initial division if you need some other kind of remainder.  For example, if I were to implement the functionality of the fmod() function in the C standard library, I’d call the two-argument remainder with RoundingMode.DOWN (instead of RoundingMode.HALF_EVEN which is what IEEEremainder() uses).

public Rational pow(int exponent);
public Rational sqr() { /* as if: */ return pow(2); }
You can raise this to some integral power.  Note that the exponent must be an integer since raising to a non-integral power yields an irrational value in general.  (2½ comes easily to mind.)
public Rational sqrt()
{
    /* as if: */
    return new Rational(Math.sqrt(doubleValue()));
}
public Rational sqrt(double accuracy)
{
    /* as if: */
    return new Rational(Math.sqrt(doubleValue()), accuracy);
}
Included are a couple of ways to approximate square roots by temporarily storing the values in doubles.  The no-argument version will likely get you really huge numerators and denominators because it calls Rational(double) which tries to do an exact conversion.  The one-argument version calls Rational(double,double) which uses a continued fractions loop and can generate much smaller numerators and denominators; but it might be slower because it does lots of BigInteger arithmetic and so bangs away at the heap.

Both will throw an ArithmeticException if this is less than zero (which would yield an imaginary number); and the constructor that gets called by the one-argument version will throw an ArithmeticException if the computed maximum error is non-finite or subnormal.

I generally balk at mimicing java.lang.Math methods that return irrational values; but I include these two as examples of how to do it if users really need someting like a rational approximation of the cosine of 2/3 (or whatever).


All suggestions and corrections will be welcome; all flames will be amusing.
Mail to was@pobox.com.