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 and a
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
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. (A less safe version that just uses longs internally is also supplied in the ZIP file mentioned at the end of this introduction.)
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.
Class invariants:
This paper, the source code, and the Boost license are available in this ZIP archive which also includes documentation and source code for a version that just uses longs internally for its numerator and denominator and so will likely be faster but runs the risk of overflow.
import java.math.BigInteger; import java.math.BigDecimal; import java.math.MathContext; 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(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(BigDecimal value, MathContext precisionAndRounding); 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(); public BigInteger toBigInteger(); public BigInteger toBigInteger(RoundingMode rounding); public BigDecimal toBigDecimal(); public BigDecimal toBigDecimal(MathContext precisionAndRounding); @Override public String toString(); public String toString(int radix); public String toString(boolean showDen1); public String toString(int radix, boolean showDen1); // // Observers: // public BigInteger numerator(); public BigInteger 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 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.
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 Rational 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. For example,Rational(Math.PI) constructs884279719003555 / 212. If you can tolerate the big numerators and denominators, this can be faster than the two-argument version immediately below because we know that Java’s doubles conform to IEEE 754, and so we can break out the sign, exponent and significand by twiddling bits and thus do most of the arithmetic in primitive types. We do only trivial BigInteger arithmetic in constant time.
This throws an IllegalArgumentException if value is non-finite (±infinity or NaN).
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 ±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 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 value is non-zero and the computed allowable error is non-finite.
public Rational(BigDecimal value); public Rational(BigDecimal value, MathContext precisionAndRounding);
These methods construct a Rational from a BigDecimal.The two-argument version makes a local copy of value, rounded according to the passed MathContext, and with trailing zeros removed; then,
The one-argument version just calls the two-argument version passing MathContext.UNLIMITED for the second argument so that no rounding occurs; and thus it constructs a Rational that’s exactly equal to its argument (but note that the argument might well be inexact to begin with if it was constructed from some binary floating point value).
- if the copy’s scale (the number of decimal digits to the right of the decimal point) is non-negative,
- it sets the numerator to the copy’s unscaled value (just the represented digits as an integer),
- sets the denominator to 10scale ,
- and reduces the fraction to lowest terms;
- otherwise (the scale is less than zero indicating the absence of trailing zeros to the left of the decimal point),
- it sets the numerator to the copy’s unscaled value × 10−scale
- and sets the denominator to one.
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.
If the string contains either a '/' or a '.', the result will be normalized such that the denominator is greater than zero and the fraction is reduced to lowest terms.
- If the string contains a '/',
- it will be taken to mean “numerator/denominator”,
- the substrings on either side of the slash must be integers,
- and you’ll get an ArithmeticException if the denominator is zero;
- otherwise,
- the string will be taken to be the exact value,
- and a '.' may be used as a radix point.
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 a BigInteger constructor; and you’ll get a NumberFormatException if that’s not the case. (Note that an empty string will get you a NumberFormatException; so there must be at least one digit both before and after a '/', and there must be at least one digit either before or after a '.'.)
public BigInteger toBigInteger(RoundingMode rounding); public BigInteger toBigInteger(); @Override public int intValue(); @Override public long longValue();
These methods round to an integer.The no-argument
toBigInteger() rounds toward zero, effectively truncating this’ fractional part; andintValue() andlongValue() do the expected narrowing conversions of that value.toBigInteger(RoundingMode) will throw an ArithmeticException if RoundingMode.UNNECESSARY is passed and this is not already an integer.
public BigDecimal toBigDecimal(MathContext precisionAndRounding); 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.
@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.)
public BigInteger numerator(); public BigInteger 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. For example,5/2 is 2 + ½.
@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 ifthis.equals(other).
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; anddivide() will throw an ArithmeticException if divisor is zero.
public Rational copySign(Rational other);
This method returns a Rational with the magnitude of this and the sign of other.
public Rational round(RoundingMode rounding);
This method just calls toBigInteger(rounding) to get the new numerator; and the new denominator will be BigInteger.ONE.
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() { /* as if: */ 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’strunc() : 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, the functionality of the
fmod() function in the C standard library can be implemented by calling the two-argumentremainder() with RoundingMode.DOWN.
public Rational pow(int exponent); public Rational sqr() { /* as if: */ return pow(2); }
You can raise this to some integral power. 00 returns 1.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.