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));
}
}
-
\$\begingroup\$ I would name the class BigRational, and extend Number and implement Comparable<BigRational> \$\endgroup\$President James K. Polk– President James K. Polk2017年08月05日 17:09:31 +00:00Commented Aug 5, 2017 at 17:09
3 Answers 3
- 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.
Since the numbers are always normalized in constructor, the method
simplify()
is useless.When you check if denominator is zero, it would be more explicit if you do it like this
denominator.equals(ZERO)
rather thandenominator.signum() == 0
Using BigInteger may be overkill. The plain
long
could be more than enough.Consider extending
java.lang.Number
. Then methodtoReal
will becomedoubleValue()
.Method
compares
violates single responsibilty principle. It does two things - compares the numbers and represents the result as text. Consider implementingjava.lang.Comparable
instead.I would replace constructor
Rational(String fraction)
with static methodvalueOf(String)
. But this is rather personal taste.There are few places where parenthesis are redundant:
BigInteger n = ( the_mcm.divide(this.denominator) ) .multiply(this.numerator);
Method
product
should be calledmultiply
to be consistent with other java classes like BigIntegerConsider implementing methods
equals
andhashCode
.Consider keeping sign in the nominator only. That will simplify
toString()
method.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.
BigInteger.gdc(BigInteger)
always returns a non-negative value, so the lineif(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 invokeInteger.parseInt(String)
and then compare the returnedint
to0
. This would guard against all sorts of loopholes, including"-0"
. Alternatively, you could first create aBigInteger
from the denominatorString
and then compare the resultingBigInteger
toBigInteger.ZERO
.- The
mod()
method is broken: Consider aRational
where the low-order 32 bits of the numerator are0x00000002
and the low-order 32 bits of the denominator are0x00000001
, and all higher-order bit-pairs are equal and at least one bit thereof is1
.mod()
should now return1
, but it actually returns0
. It would also throw anArithmeticException
if the low-order 32 bits of the denominator are all0
, 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 aBigInteger
, and only then, if you really want to return anint
instead of aBigInteger
, convert the result to anint
. Your method
toReal()
checks the denominator for being zero. But thedenominator
of aRational
can/should not be zero in the first place – the constructors already see to that. Ifdenominator
is zero whentoReal()
is invoked, then there is a bug in your code. Instead of throwing aRuntimeException
, there is a language construct specifically designed for this purpose: An assertion:assert !this.denominator.equals(ZERO);
If
denominator
equalsZERO
, then anAssertionError
will be thrown. AnAssertionError
is also aThrowable
, so the effect is pretty much the same as if you throw anArithmeticException
, but anAssertionError
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 fortoString()
. Be aware, however, that assertions are disabled by default, so the expression in theassert
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 onlynumerator
). - You confused the evaluations based on the signum: If
expression.signum() == -1
, thenthis
is less thanq
, and vice versa. - The algorithm is faulty: Comparing -2/1 to 3/-1 will produce incorrect results (it is possible that
numerator
is positive anddenominator
is negative)
- In the second line, it should probably be
- In the method
toString()
: Returning"" + numerator
might save 6 characters of code, but, program-logic-wise, it is less direct that simply writingnumerator.toString()
and therefore more confusing to read. opposite()
is a rather meaningless method name, mathematically speaking. A more informative name might beadditiveInverse()
, or, analogous to theBigInteger
equivalent,negate()
.- There's a potential typo in
add()
: Could it be thatmcm
should actually belcm
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 int
s (or alternatively long
s, as ponomandr has suggested), and reap the benefits of using primitive values instead of BigInteger
s (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 forq.numerator
being zero, notq.denominator
.
-
\$\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– Tobal2017年08月08日 10:14:34 +00:00Commented Aug 8, 2017 at 10:14
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;
}
}
-
\$\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– Stingy2017年08月08日 16:22:07 +00:00Commented Aug 8, 2017 at 16:22