A Rational Number in Java
that uses longs internally

Bill Seymour
2025-02-16


Contents


Introduction

This paper describes the public API of an open-source rational number in Java that uses longs internally for its numerator and denominator.  It’s distributed under the Boost Software License.  (This is not part of Boost.  The author just likes their permissive license.)

Another version of this class that uses BigIntegers internally to guard against overflow is also available in the ZIP file mentioned at the end of this introduction.

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 as examples of how users might mimic Math methods that return irrationals.

Since almost every operation on rational numbers requires multiplication behind the scenes, numerators and denominators can get big in a hurry; and so there’s a danger of overflowing the longs used internally.  This version is best used only if you know that you’re dealing with fractions that have reasonably small numerators and denominators and you don’t do lots of arithmetic.  The two constructors that take doubles as arguments are the only methods that check for overflow.

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.

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 normal double; and since the class claims to extend Number, it needs to override floatValue() and doubleValue() in any event.

Class invariants:

This paper, the source code, and the Boost license are available in this ZIP archive which also includes the documentation and source code for the original version which uses BigIntegers internally.  The source code for this version is in LRat.java, so you’ll need to rename that file to Rational.java if you’re OK with just using longs for the numerator and denominator.


Synopsis

import java.math.RoundingMode;

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 ONE_HALF;

    //
    // Constructors:
    //
    public Rational();

    public Rational(long value);

    public Rational(long numerator, long denominator);

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

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

    //
    // Conversions to other types:
    //
    @Override public int intValue();
    @Override public long longValue();
    @Override public float floatValue();
    @Override public double doubleValue();

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

    //
    // Observers:
    //
    public long numerator();
    public long denominator();
    public int signum();
    public Rational[] getParts();

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

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

    //
    // Arithmetic operations:
    //
    public Rational negate();
    public Rational abs();
    public Rational reciprocal();

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

    public Rational copySign(Rational other);

    public Rational round(RoundingMode rounding);
    public Rational round();
    public Rational ceil();
    public Rational floor();
    public Rational rint();
    public Rational trunc();

    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 ONE_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);
This one-argument constructor constructs a Rational with a numerator equal to value and a denominator equal to one.
public Rational(long numerator, long denominator);
This two-argument constructor constructs a Rational with the specified numerator and denominator, and then normalizes 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, this method 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.  For example, Rational(Math.PI) constructs 884279719003555 / 281474976710656.

This’ll probably be a good choice only if the argument is an integral power of 2 or if you really do need an extremely accurate conversion; but if you can tolerate the huge numerators and denominators, it’ll be a lot faster than the continued fractions version immediately below.

This throws an IllegalArgumentException if value is non-finite or subnormal; and it throws an ArithmeticException if either the numerator or the denominator would overflow a long.  You’re safe if the absolute value of the argument is in the open interval, {2−63,211}; but the open upper bound could be as high as 263 depending on how many trailing zeros there are in the significand.  (For example, you’ll get an upper bound of 263 if value is an integral power of 2 giving a significand of all zeros.)

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 pass through the loop; but it might be slower if you request a very accurate conversion.

This method will throw an IllegalArgumentException if value is non-finite; and it will throw an ArithmeticException if Math.abs(value) is greater than Long.MAX_VALUE or if the computed maximum error is non-finite or subnormal.

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 '.' if there is one), must make sense when passed to Long.parseLong(String,int); and you’ll get a NumberFormatException if that’s not the case.

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.


Conversions to other types

@Override public int intValue();
@Override public long longValue();
@Override public float floatValue();
@Override public double doubleValue();
These should be unsurprising.
@Override 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.  (Note that the only way to get “/1” in the string is to explicitly pass true as the only or second argument.)


Observers

public long numerator();
public long denominator();
You can inspect the numerator and denominator independently.
public int signum();
signum() returns +1 if this is greater than zero, 0 if this is equal to zero, or −1 if this is less than zero.
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.


Comparisons

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

public Rational min(Rational other);
public Rational max(Rational other);
These are all expected to be unsurprising.  min() and max() both return this if this.equals(other).


Arithmetic operations

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

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.  reciprocal() will throw an ArithmeticException if this is zero; and divide() will throw an ArithmeticException if divisor is zero.
public Rational copySign(Rational other);
This method returns a Rational with the value of this but with the sign of other.
public Rational round(RoundingMode rounding);
This method rounds to an integer as specified by the passed java.math.RoundingMode.
public Rational round() { return round(RoundingMode.HALF_UP); }
public Rational ceil()  { return round(RoundingMode.CEILING); }
public Rational floor() { return round(RoundingMode.FLOOR); }
public Rational rint()  { return round(RoundingMode.HALF_EVEN); }
public Rational trunc() { return round(RoundingMode.DOWN); }
These methods generally mimic the behavior of the java.lang.Math methods of the same name.  trunc() mimics the C standard library’s trunc():  it rounds toward zero effectively truncating this’ fractional part.
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 we were to implement the functionality of the fmod() function in the C standard library, we’d call the two-argument remainder with RoundingMode.DOWN.

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.)

If this is zero and exponent is negative, pow will throw an ArithmeticException since we’re trying to take the reciprocal of zero.

public Rational sqrt()
{
    /* make sure this is non-negative, then: */
    return new Rational(Math.sqrt(doubleValue()));
}
public Rational sqrt(double accuracy)
{
    /* make sure this is non-negative, then: */
    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.

Both will throw an ArithmeticException if this is less than zero (which would yield an imaginary number); and the constructors that get called can throw IllegalArgumentExceptions or ArithmeticExceptions if the numerator or denominator could overflow.

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.