package math.number;

/**
 *<pre>
 *  Immutable ADT for Rational numbers. 
 * 
 *  Invariants
 *  -----------
 *   - gcd(num, den) = 1, i.e, the rational number is in reduced form
 *   - den >= 1, the denominator is always a positive integer
 *   - 0/1 is the unique representation of 0
 *
 *  I employ some tricks to stave of overflow, but if you
 *  need arbitrary precision rationals, use BigRational.java.</pre>
 *  
 * @author Daniel Weschke
 */
public class Rational implements Comparable<Rational>{
	
    public static final Rational ZERO = new Rational(0);
    

    /**
     * numerator
     */
    private int num;
    
    /**
     * denominator
     */
    private int den;
	
	/**
	 * create a new zero Rational object with denominator 1.
	 */
	public Rational(){
		den = 1;
	}
	
	/**
	 * create and initialize a new Rational object.
	 * @param numerator
	 */
	public Rational(int numerator){
		this();
		num = numerator;
	}

    /**
     * create and initialize a new Rational object.
     * @param numerator
     * @param denominator
     */
    public Rational(int numerator, int denominator){

        // deal with x/0
        if (denominator == 0) throw new IllegalArgumentException("Denominator is zero.");

        // reduce fraction
        int g = gcd(numerator, denominator);
        num = numerator   / g;
        den = denominator / g;

        // only needed for negative numbers
        if(den < 0) { den = -den; num = -num; }
    }

	/**
	 * number to rational
	 * @param number
	 */
	public Rational(double number) {

		// convert to string to avoid rounding error
		String ds = "" + number;

		// split at point, attention: point is a special character, therefore replaced with p
		ds = ds.replace(".", "p");
		String[] sa = ds.split("p");

		// determine numerator and denominator
		den = (int) Math.pow(10, sa[1].length());
		num = (int) (number * den);

		// reduce fraction
		int factor = gcd(num, den);
		den /= factor;
		num /= factor;
		
		assert den != 0 : "Denominator is zero!";
		assert 1.0 * num / den == number : "Rounding error!!";
	}

    /**
     * @return the numerator
     */
    public int getNumerator(){ return num; }
    
    /**
     * @return the denominator
     */
    public int getDenominator(){ return den; }

    /**
     * return a + b, staving off overflow
     * @param b
     * @return a + b
     */
    public Rational plus(Rational b){
        Rational a = this;

        // special cases
        if (a.compareTo(ZERO) == 0) return b;
        if (b.compareTo(ZERO) == 0) return a;

        // Find gcd of numerators and denominators
        int f = gcd(a.num, b.num);
        int g = gcd(a.den, b.den);

        // add cross-product terms for numerator
        Rational s = new Rational((a.num / f) * (b.den / g) + (b.num / f) * (a.den / g),
                                  lcm(a.den, b.den));

        // multiply back in
        s.num *= f;
        return s;
    }

    /**
     * @param b
     * @return a - b
     */
    public Rational minus(Rational b){
        Rational a = this;
        return a.plus(b.negate());
    }

    /**
     * @return -a
     */
    public Rational negate(){
        return new Rational(-num, den);
    }

    /**
     * return a * b, staving off overflow as much as possible by cross-cancellation
     * @param b
     * @return a * b
     */
    public Rational times(Rational b) {
        Rational a = this;

        // reduce p1/q2 and p2/q1, then multiply, where a = p1/q1 and b = p2/q2
        Rational c = new Rational(a.num, b.den);
        Rational d = new Rational(b.num, a.den);
        return new Rational(c.num * d.num, c.den * d.den);
    }

    /**
     * @param b
     * @return a / b
     */
    public Rational over(Rational b){
        Rational a = this;
        return a.times(b.reciprocal());
    }

    /**
     * reciprocal, old denominator as numerator / old numerator as denominator
     * @return reciprocal 1/x
     */
    public Rational reciprocal(){ return new Rational(den, num); }

    /**
     * create and return a new rational (r.num + s.num) / (r.den + s.den)
     * @param r
     * @param s
     * @return mediant of two rationals
     */
    public static Rational mediant(Rational r, Rational s){
        return new Rational(r.num + s.num, r.den + s.den);
    }

    /**
     * returns greatest common divisor using Euclid's algorithm
     * @param m
     * @param n
     * @return gcd(|m|, |n|)
     */
    private static int gcd(int m, int n){
        if (m < 0) m = -m;
        if (n < 0) n = -n;
        if (0 == n) return m;
        else return gcd(n, m % n);
    }

    /**
     * returns least/lowest common multiple
     * @param m
     * @param n
     * @return lcm(|m|, |n|)
     */
    private static int lcm(int m, int n){
        if(m < 0) m = -m;
        if(n < 0) n = -n;
        return m * (n / gcd(m, n));    // parentheses important to avoid overflow
    }

    /**
     * @return double precision representation
     */
    public double toDouble(){
        return (double) num / den;
    }

    /**
     * return string representation
     */
    @Override
	public String toString(){ 
        if (den == 1) return num + "";
        else          return num + "/" + den;
    }

    /**
     * return { -1, 0, +1 } if a < b, a = b, or a > b
	 * 
	 * @param b compare rational
	 * @return Bitcode:<br />
	 * <code> 1</code> rational is greater<br />
	 * <code> 0</code> rational is in equal size<br />
	 * <code>-1</code> rational is smaller<br />
     */
    @Override
	public int compareTo(Rational b){
        Rational a = this;
        int lhs = a.num * b.den;
        int rhs = a.den * b.num;
        if (lhs < rhs) return -1;
        if (lhs > rhs) return +1;
        return 0;
    }

    /**
     * is this Rational object equal to y?
     */
    @Override
	public boolean equals(Object y){
        if (y == null) return false;
        if (y.getClass() != this.getClass()) return false;
        Rational b = (Rational) y;
        return compareTo(b) == 0;
    }

    /**
     * hashCode consistent with equals() and compareTo()
     */
    @Override
	public int hashCode(){
        return this.toString().hashCode();
    }


    // test client
    public static void main(String[] args){
        Rational x, y, z;

        // 1/2 + 1/3 = 5/6
        x = new Rational(1, 2);
        y = new Rational(1, 3);
        z = x.plus(y);
        System.out.println(x + " + " + y + " = " + z);

        // 8/9 + 1/9 = 1
        x = new Rational(8, 9);
        y = new Rational(1, 9);
        z = x.plus(y);
        System.out.println(x + " + " + y + " = " + z);

        // 1/200000000 + 1/300000000 = 1/120000000
        x = new Rational(1, 200000000);
        y = new Rational(1, 300000000);
        z = x.plus(y);
        System.out.println(x + " + " + y + " = " + z);

        // 1073741789/20 + 1073741789/30 = 1073741789/12
        x = new Rational(1073741789, 20);
        y = new Rational(1073741789, 30);
        z = x.plus(y);
        System.out.println(x + " + " + y + " = " + z);

        //  4/17 * 17/4 = 1
        x = new Rational(4, 17);
        y = new Rational(17, 4);
        z = x.times(y);
        System.out.println(x + " * " + y + " = " + z);

        // 3037141/3247033 * 3037547/3246599 = 841/961 
        x = new Rational(3037141, 3247033);
        y = new Rational(3037547, 3246599);
        z = x.times(y);
        System.out.println(x + " * " + y + " = " + z);

        // 1/6 - -4/-8 = -1/3
        x = new Rational( 1,  6);
        y = new Rational(-4, -8);
        z = x.minus(y);
        System.out.println(x + " - " + y + " = " + z);

        double a;
        a = 1.75;
        z = new Rational(a);
        System.out.println(a + " = " + z);

        a = 1/3d;
        z = new Rational(a);
        System.out.println(a + " = " + z);

        z = new Rational(1,0);
        System.out.println("1/0" + " = " + z);
    }

}