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
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.
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 or a BigDecimal; you can convert a
Rational to a BigDecimal; and since the class claims to extend Number,
it needs to override
Class invariants:
This paper, the source code, and the Boost license are available in this ZIP archive.
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(long numerator, long denominator); public Rational(BigInteger value); 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); // // 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 BigInteger toBigInteger(); public BigInteger toBigInteger(RoundingMode rounding); public BigDecimal toBigDecimal(); public BigDecimal toBigDecimal(MathContext precisionAndRounding); @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); // // 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 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); }
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. 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.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 significand and exponent 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 or subnormal.
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 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); 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 is >= zero,
- it sets the numerator to the copy’s unscaled value,
- 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.
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.
- If the string contains a '/',
- it will be taken to mean “numerator/denominator”, and the substrings on either side of the slash must be integers;
- otherwise,
- the string will be taken to be the exact value, and a '.' may be used as a radix point.
Issue: the BigInteger constructor requires at least one digit. Would it be better to treat a zero-length string as "0"?
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.
@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.
public BigInteger toBigInteger(RoundingMode rounding); public BigInteger toBigInteger(); @Override public int intValue(); @Override public long longValue();
These methods round to an integer. The no-argumenttoBigInteger() callstoBigInteger(RoundingMode.DOWN) which has the effect of truncating the fractional part, i.e., rounding toward zero.intValue() and longValue() quietly do narrowing conversions of the value returned by toBigInteger().
toBigInteger(RoundingMode) will throw an ArithmeticException if RoundingMode.UNNECESSARY is passed and this is not already an integer. It can also throw an IllegalArgumentException if the argument is not actually one of the RoundingMode enumerators, but that seems unlikely.
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 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. reciprocal() will throw an ArithmeticException if this is zero; anddivide() will throw an ArithmeticException if divisor is zero.
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() { 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, if we were to implement the functionality of the
fmod() function in the C standard library, we’d call 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. 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 callsRational(double) which tries to do an exact conversion. The one-argument version callsRational(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).