2
\$\begingroup\$

Here is a class to handle fractions with Java using BigInteger because it supports gcd calculus. I don't know if BigInteger is the optimal use with fractions, but this is an alternative with fractions. This class supports the most common fractional operations. It's a pity Java doesn't support the operators overloading like C++. The code:

import java.math.BigInteger;
import java.util.regex.Pattern;

public class Rational {
    private BigInteger numerator;
    private BigInteger denominator;
    private static final BigInteger ZERO = new BigInteger("0");
    private static final BigInteger ONE = new BigInteger("1");

    private void  normalize(){
        BigInteger the_gcd = numerator.gcd(denominator);
        if(the_gcd.signum() == -1) the_gcd = the_gcd.abs();
        if(numerator.signum() == -1 && denominator.signum() == -1){
            numerator = numerator.abs();
            denominator = denominator.abs();
        }
        numerator = numerator.divide(the_gcd);
        denominator = denominator.divide(the_gcd);
    }
    public Rational opposite(){
        return new Rational(this.numerator.negate(), this.denominator);
    }

    public Rational absolute() {
        if(this.numerator.signum() == 1 && this.denominator.signum() == 1) return this;
        else return this.opposite();
    }

    public Rational(BigInteger numerator, BigInteger denominator) {
        if(denominator.signum() == 0)
            throw new IllegalArgumentException("The denominator can't be ZERO.");
        this.numerator = numerator;
        this.denominator = denominator;
        normalize();
    }

    public Rational(String fraction){
        fraction = fraction.replaceAll(" ","");
        if(!fraction.contains("/")){
            this.numerator = new BigInteger(fraction);
            this.denominator = BigInteger.ONE;
        }
        else {
            String[] fraction1 = fraction.split(Pattern.quote("/"));
            if(fraction1[1].equals("0"))
                throw new IllegalArgumentException("The denominator can't be ZERO.");
            this.numerator = new BigInteger(fraction1[0]);
            this.denominator = new BigInteger(fraction1[1]);
        }
        normalize();
    }
    public Rational add(Rational q) {
        BigInteger product = this.denominator.multiply(q.denominator);
        BigInteger the_mcm = product.divide(this.denominator.gcd(q.denominator));
        BigInteger n = (the_mcm.divide(this.denominator)).multiply(this.numerator);
        n = n.add((the_mcm.divide(q.denominator)).multiply(q.numerator));
        return new Rational(n, the_mcm);
    }
    public Rational subtract(Rational q){
        return this.add(q.opposite());
    }
    public Rational product(Rational q){
        return new Rational(this.numerator.multiply(q.numerator), this.denominator.multiply(q.denominator));
    }
    public Rational divide(Rational q){
        if(q.denominator.signum() == 0) throw new ArithmeticException("Divided by ZERO is illegal.");
        return new Rational(this.numerator.multiply(q.denominator), this.denominator.multiply(q.numerator));
    }
    public int mod(){
        return this.numerator.intValue() % this.denominator.intValue();
    }
    public Rational inverse(){
        if(this.numerator.signum() == 0) throw new ArithmeticException("Not exist the inverse when the numerator is ZERO.");
        return new Rational(this.denominator, this.numerator);
    }
    public Rational pow(int n){
        return new Rational(this.numerator.pow(n), this.denominator.pow(n));
    }
    public double toReal(){
        if(this.denominator.equals(ZERO)) throw new ArithmeticException("It's not a number, NAN");
        return this.numerator.doubleValue() / this.denominator.doubleValue();
    }
    public String compares(Rational q){
        BigInteger expression1 = this.numerator.multiply(q.denominator);
        BigInteger expression2 = this.denominator.multiply(numerator);
        BigInteger expression = expression1.subtract(expression2);
        if(expression.signum() == -1) return this.toString() + " > " + q.toString();
        else if(expression.signum() == 1) return this.toString() + " < " + q.toString();
        else return this.toString() + " = " + q.toString();
    }
    public Rational simplify(){
        normalize();
        return new Rational(this.numerator, this.denominator);
    }
    @Override
    public String toString() {
        if(numerator.equals(ZERO) && ! denominator.equals(ZERO))
            return "0";
        if(denominator.equals(ONE)) return "" + numerator;
        else if((numerator.signum() == -1 && denominator.signum() == -1) ||
                (numerator.signum() == 1 && denominator.signum() == -1))
            return numerator.negate() + " / " + denominator.negate();
        else return numerator + " / " + denominator;
    }
}

The class contains 2 different constructors, sum, subtract, product, division, opposite, inverse, simplify... Here's a short main code to practice:

import java.math.BigInteger;
import java.util.regex.Pattern;

public class Main {
    public static void main(String[] args) {
        Rational p = new Rational(new BigInteger("1"), new BigInteger("-2"));
        Rational q = new Rational(new BigInteger("3"), new BigInteger("4"));
        Rational p_plus_q = p.add(q);
        Rational p_prod_q = p.product(q);
        Rational p_div_q = p.divide(q);

        Rational p_minus_q = p.subtract(q);
        System.out.println(p_plus_q);
        System.out.println(p_minus_q);
        System.out.println(p_prod_q);
        System.out.println(p_div_q);
        System.out.println(p.absolute());
        System.out.println(p.compares(q));

        Rational r = new Rational(new BigInteger("-4"), new BigInteger("6"));
        System.out.println(r.simplify());
        System.out.println(r.pow(2));
        System.out.println(r.inverse());
        System.out.println(r.toReal());
        System.out.println(r.mod());

        Rational q1 = new Rational("-8/5");
        Rational q2 = new Rational("3 / 4");
        Rational q3 = new Rational("2");

        System.out.println("WITH CHARACTERS");
        System.out.println("===============");
        System.out.println(q1.product(q2));
        System.out.println(q1.product(q3));
    }
}
\$\endgroup\$
  • \$\begingroup\$ I would name the class BigRational, and extend Number and implement Comparable<BigRational> \$\endgroup\$ – James K Polk Aug 5 '17 at 17:09
4
\$\begingroup\$
  1. These two constants are redundant:
private static final BigInteger ZERO = new BigInteger("0");
private static final BigInteger ONE = new BigInteger("1");

Use java.math.BigInteger.ZERO and BigInteger.ONE instead.

  1. Since the numbers are always normalized in constructor, the method simplify() is useless.

  2. When you check if denominator is zero, it would be more explicit if you do it like this denominator.equals(ZERO) rather than denominator.signum() == 0

  3. Using BigInteger may be overkill. The plain long could be more than enough.

  4. Consider extending java.lang.Number. Then method toReal will become doubleValue().

  5. Method compares violates single responsibilty principle. It does two things - compares the numbers and represents the result as text. Consider implementing java.lang.Comparable instead.

  6. I would replace constructor Rational(String fraction) with static method valueOf(String). But this is rather personal taste.

  7. There are few places where parenthesis are redundant:

    BigInteger n = ( the_mcm.divide(this.denominator) ) .multiply(this.numerator);

  8. Method product should be called multiply to be consistent with other java classes like BigInteger

  9. Consider implementing methods equals and hashCode.

  10. Consider keeping sign in the nominator only. That will simplify toString() method.

  11. Methods should be orderded (constructors go first, other methods should be declared after methods where they are used) and undercore in the middle of variable names should be avoided. See Java code conventions.

\$\endgroup\$
2
\$\begingroup\$
  • BigInteger.gdc(BigInteger) always returns a non-negative value, so the line

    if(the_gcd.signum() == -1) the_gcd = the_gcd.abs();
    

    in normalize() is redundant.

  • Your Rational(String) constructor can be easily tricked into accepting a zero denominator:

    new Rational("5/00")
    

    A safer way to check a String for representing an integer with the value of 0 would be to invoke Integer.parseInt(String) and then compare the returned int to 0. This would guard against all sorts of loopholes, including "-0". Alternatively, you could first create a BigInteger from the denominator String and then compare the resulting BigInteger to BigInteger.ZERO.

  • The mod() method is broken: Consider a Rational where the low-order 32 bits of the numerator are 0x00000002 and the low-order 32 bits of the denominator are 0x00000001, and all higher-order bit-pairs are equal and at least one bit thereof is 1. mod() should now return 1, but it actually returns 0. It would also throw an ArithmeticException if the low-order 32 bits of the denominator are all 0, because then an illegal division by zero would be attempted. Your method would be more stable and predictable if you first calculated the modulo as a BigInteger, and only then, if you really want to return an int instead of a BigInteger, convert the result to an int.
  • Your method toReal() checks the denominator for being zero. But the denominator of a Rational can/should not be zero in the first place – the constructors already see to that. If denominator is zero when toReal() is invoked, then there is a bug in your code. Instead of throwing a RuntimeException, there is a language construct specifically designed for this purpose: An assertion:

    assert !this.denominator.equals(ZERO);
    

    If denominator equals ZERO, then an AssertionError will be thrown. An AssertionError is also a Throwable, so the effect is pretty much the same as if you throw an ArithmeticException, but an AssertionError carries a completely different meaning (i.e. that there's a bug in your code, as opposed to someone recklessly tried to divide by zero). The same goes for toString(). Be aware, however, that assertions are disabled by default, so the expression in the assert statement will not be evaluated unless you enable them.

  • There are three bugs in your compares(Rational) method:
    • In the second line, it should probably be q.numerator instead of only numerator).
    • You confused the evaluations based on the signum: If expression.signum() == -1, then this is less than q, and vice versa.
    • The algorithm is faulty: Comparing -2/1 to 3/-1 will produce incorrect results (it is possible that numerator is positive and denominator is negative)
  • In the method toString(): Returning "" + numerator might save 6 characters of code, but, program-logic-wise, it is less direct that simply writing numerator.toString() and therefore more confusing to read.
  • opposite() is a rather meaningless method name, mathematically speaking. A more informative name might be additiveInverse(), or, analogous to the BigInteger equivalent, negate().
  • There's a potential typo in add(): Could it be that mcm should actually be lcm for "least common multiple"?

A final comment on using BigInteger. You list the fact that it has a built-in method for calculating the greatest common divisor as the only reason for using it. But a method for calculating the gcd is easily written in a few lines. If this is really your only reason for using BigInteger, then you might consider writing a simple method for calculating the gcd of two ints (or alternatively longs, as ponomandr has suggested), and reap the benefits of using primitive values instead of BigIntegers (e.g., as you have noticed, the use of operators). Of course, on the other hand, you would also have to deal with their limited range.

Update

  • I found another bug: In divide(Rational), you need to check for q.numerator being zero, not q.denominator.
\$\endgroup\$
  • \$\begingroup\$ Well thanks for your advise in compare method, now I've fixed it. divide(Rational) is correct with denominator, you can't divide with denominator zero, i've add when both are zero. I put mcm because i'm spanish. Read below please \$\endgroup\$ – Tobal Aug 8 '17 at 10:14
0
\$\begingroup\$

Ok, I've made some bug fixes thanks to Stringy and Ponomamndr. I've added getters and setters. I've added hashCode overwrite and equals. I've used BigInteger because two reasons: 1) there's a gcd function, so if Java use BigInteger for gcd is because there's a good reason for it. 2) because this is another way to learn how to implement Rationals with Java.

In other terms, it's absurd that Math Java Class not contain gcd and lcd functions (methods) by default.

There's a simplify method not for me, it is for the user, nothing else.

The new fixed code:

import java.math.BigInteger;
import java.util.regex.Pattern;

public class Rational implements Comparable<Rational>{
    private BigInteger numerator;
    private BigInteger denominator;
    private static final BigInteger ZERO = new BigInteger("0");
    private static final BigInteger ONE = new BigInteger("1");

    public Rational() {
        this(ZERO, ONE);
    }

    public Rational(BigInteger numerator, BigInteger denominator) {
        if(denominator.equals(ZERO))
            throw new IllegalArgumentException("The denominator can't be ZERO.");
        this.numerator = numerator;
        this.denominator = denominator;
        normalize();
    }

    public Rational(String fraction){
        fraction = fraction.replaceAll(" ","");
        if(!fraction.contains("/")){
            this.numerator = new BigInteger(fraction);
            this.denominator = BigInteger.ONE;
        }
        else {
            String[] fraction1 = fraction.split(Pattern.quote("/"));
            if(fraction1[1].equals("0"))
                throw new IllegalArgumentException("The denominator can't be ZERO.");
            this.numerator = new BigInteger(fraction1[0]);
            this.denominator = new BigInteger(fraction1[1]);
        }
        normalize();
    }
    public BigInteger getNumerator() {
        return numerator;
    }

    public void setNumerator(BigInteger numerator) {
        this.numerator = numerator;
    }

    public BigInteger getDenominator() {
        return denominator;
    }

    public void setDenominator(BigInteger denominator) {
        this.denominator = denominator;
    }
    private void  normalize(){
        BigInteger the_gcd = numerator.gcd(denominator);

        if(numerator.signum() == -1 && denominator.signum() == -1){
            numerator = numerator.abs();
            denominator = denominator.abs();
        }
        numerator = numerator.divide(the_gcd);
        denominator = denominator.divide(the_gcd);
    }
    public Rational opposite(){
        return new Rational(this.numerator.negate(), this.denominator);
    }

    public Rational absolute() {
        if(this.numerator.signum() == 1 && this.denominator.signum() == 1) return this;
        else return this.opposite();
    }

    public Rational add(Rational q) {
        BigInteger product = this.denominator.multiply(q.denominator);
        BigInteger the_lcm = product.divide(this.denominator.gcd(q.denominator));
        BigInteger n = (the_lcm.divide(this.denominator)).multiply(this.numerator);
        n = n.add((the_lcm.divide(q.denominator)).multiply(q.numerator));
        return new Rational(n, the_lcm);
    }
    public Rational subtract(Rational q){
        return this.add(q.opposite());
    }
    public Rational multiply(Rational q){
        return new Rational(this.numerator.multiply(q.numerator), this.denominator.multiply(q.denominator));
    }
    public Rational divide(Rational q){
        if(q.denominator.equals(ZERO) || (q.numerator.equals(ZERO) && q.denominator.equals(ZERO)))
            throw new ArithmeticException("Divided by ZERO is illegal.");
        return new Rational(this.numerator.multiply(q.denominator), this.denominator.multiply(q.numerator));
    }
    public BigInteger mod(){
        return this.numerator.remainder(this.denominator);
    }
    public Rational inverse(){
        if(this.numerator.equals(ZERO))
            throw new ArithmeticException("Not exist the inverse when the numerator is ZERO.");
        return new Rational(this.denominator, this.numerator);
    }
    public Rational pow(int n){
        return new Rational(this.numerator.pow(n), this.denominator.pow(n));
    }
    public double toReal(){
        assert !this.denominator.equals(ZERO);
        return this.numerator.doubleValue() / this.denominator.doubleValue();
    }
    public String compares(Rational q){
        Integer expression1 = this.numerator.multiply(q.denominator).intValueExact();
        Integer expression2 = this.denominator.multiply(q.numerator).intValueExact();
        if(expression1 < expression2) return this.toString() + " < " + q.toString();
        else if(expression1 > expression2) return this.toString() + " > " + q.toString();
        else return this.toString() + " = " + q.toString();
    }
    public Rational simplify(){
        normalize();
        return new Rational(this.numerator, this.denominator);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Rational rational = (Rational) o;

        return numerator.equals(rational.numerator) && denominator.equals(rational.denominator);
    }

    @Override
    public int hashCode() {
        int result = numerator.hashCode();
        result = 31 * result + denominator.hashCode();
        return result;
    }

    @Override
    public String toString() {
        if(numerator.equals(ZERO) && ! denominator.equals(ZERO))
            return "0";
        if(denominator.equals(ONE)) return "" + numerator;
        else if((numerator.signum() == -1 && denominator.signum() == -1) ||
                (numerator.signum() == 1 && denominator.signum() == -1))
            return numerator.negate() + " / " + denominator.negate();
        else return numerator + " / " + denominator;
    }

    @Override
    public int compareTo(Rational q) {
        if (this.subtract(q).numerator.signum() == 1)
            return 1;
        else if (this.subtract(q).numerator.signum() == -1)
            return -1;
        else
            return 0;
    }
}
\$\endgroup\$
  • \$\begingroup\$ I don't know why you post this here as an answer, but if you want the new version of your code to be reviewed, you should post it as a new question (and possibly link to this question). \$\endgroup\$ – Stingy Aug 8 '17 at 16:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.