/**
 * A class that represents fractions (rational numbers)
 * @author Derek Bridge
 */
public class Fraction
{
/* =======================================================================
       CONSTRUCTORS
   =======================================================================
*/
   /**
    * Allocates a new Fraction object with the given numerator and
    * denominator.
    *
    * @param aNum the numerator of this fraction.
    * @param aDenom the denominator of this fraction; cannot be zero.
    * To allocate a negative fraction, either parameter may be negative.
    * But if both are negative, a positive fraction is allocated.
    */
   public Fraction(final int aNum, final int aDenom)
   {  // Allocating a fraction representing zero is a special case
      // The denominator is always reduced to 1
      if (aNum == 0)
      {  num = 0;
         denom = 1;
      }
      else 
      {  int n = aNum; // n will hold the new numerator.
         int d = aDenom; // d will hold the new denominator.
         // The greatest common divisor (gcd) method requires
         // positive integer arguments.
         // aNum != 0 (handled above).
         // aDenom != 0 (precondition of this constructor).
         // Taking their absolute values therefore is enough to give
         // positive arguments (>0).
         int g = gcd(Math.abs(aNum), Math.abs(aDenom));
         // Reduce num and denom (if the gcd is not 1, then some larger 
         // number wholly divides both)
         n = n / g;
         d = d / g;
         // If denominator is negative, move its sign to the numerator.
         if (aDenom < 0)
         {  n = - n;
            d = - d;
         }
         // Now move n and d to the instance variables.
         num = n;
         denom = d;
      }
   }

   /**
    * Allocates a new Fraction object to represent the given integer.
    *
    * @param n the integer to be represented as a fraction.
    */
   public Fraction(final int n)
   {  num = n;
      denom = 1;
   }

/* =======================================================================
       PUBLIC INTERFACE
   =======================================================================
*/

/* --Getters----------------------------------------------------------- */

   /**
    * Returns true if and only if this fraction is 0.0.
    *
    * @return true if the fraction is zero; false otherwise.
    */
   public boolean isZero()
   {  return (num == 0 && denom == 1);
   }

   /**
    * Returns true if and only if this fraction is an integer.
    *
    * @return true if the fraction is an integer; false otherwise.
    */
   public boolean isInteger()
   {  return denom == 1;
   }

   /**
    * Returns a new fraction that represents the sum of this fraction and
    * the specified fraction.
    * @param aFraction the fraction we are adding.
    * @return a fraction that represents the sum of this fraction and the 
    * parameter.
    */
   public Fraction plus(final Fraction aFraction)
   {  return new Fraction( 
         this.num * aFraction.denom + aFraction.num * this.denom,
         denom * aFraction.denom);
   }

   /**
    * Returns a new fraction that represents the difference between this
    * fraction and the specified fraction.
    * @param aFraction the fraction we are subtracting.
    * @return a fraction that represents the difference between this
    * fraction and the parameter.
    */
   public Fraction minus(final Fraction aFraction)
   {  return new Fraction(
         this.num * aFraction.denom - aFraction.num * this.denom,
         denom * aFraction.denom);
   }
                      
/* --Common object interface------------------------------------------- */

   /**
    * Determines whether the specified object is equal to this fraction.
    * The result is true if and only if the specified object is non-null,
    * is a fraction and denotes the same fraction as this fraction.
    *
    * @param anObject the object to compare this fraction against.
    * @return true if the objects are the same; false otherwise.
    */
   public boolean equals(final Object anObject)
   {  if (anObject == null)
      {  return false;
      }
      if (! (anObject instanceof Fraction))
      {  return false;
      }
      // At this point, we know that anObject is non-null and
      // a reference to a fraction, so it can be cast to a 
      // fraction and its representation can be compared.
      Fraction f = (Fraction) anObject;
      return (this.num == f.num && this.denom == f.denom);
   }

   /**
    * Returns a string representation of this fraction.
    * These string representations will show the fraction
    * in reduced form. E.g. you will never see "4/6"; you will
    * only ever see "2/3".
    *
    * @return a string representation of this fraction.
    */
   public String toString()
   {  return (num + "/" + denom); 
   }

/* =======================================================================
       HELPER METHODS
   =======================================================================
*/

   /*
    * Returns the greatest common divisor (gcd) of two positive integers.
    *
    * @param a - any int; must be > 0.
    * @param b - any int; must be > 0.
    * @return the gcd of a and b.
    *
    * This is the recursive version of Euclid's algorithm.
    * See R.Davies: Introductory Java for Scientists and Engineers
    *     Addison-Wesley, 1999, pp.169-170.
    */
   private static int gcd(final int a, final int b)
   {  if (b != 0)
      {  return gcd(b, a % b);
      }
      else
      {  return a;
      }
   }

/* =======================================================================
       INSTANCE VARIABLES & CLASS VARIABLES
   =======================================================================
*/

   /*
    * Fractions will be stored in reduced form.
    * E.g. 2/4 is stored as 1/2, i.e. num = 1, denom = 2.
    * The numerator, num, carries the sign of the fraction.
    * Hence, denom will always be positive and can never be zero.
    * The fraction representing zero will be stored as num = 0, denom = 1.
    */
   private int num; // the fraction's numerator.
   private int denom; // the fraction's denominator.

/* =======================================================================
       TEST DRIVER
   =======================================================================
*/
   public static void main(String[] args)
   {  Fraction f;
      String s;

      f = new Fraction(3);
      s = f.toString();      
      if (! (s.equals("3/1")))
      {  System.out.println("Error05: " + s + " should be 3/1");
      }
 
      f = new Fraction(-3);
      s = f.toString();
      if (! (s.equals("-3/1")))
      {  System.out.println("Error10: " + s + " should be -3/1");
      }     

      f = new Fraction(0);
      s = f.toString();
      if (! (s.equals("0/1")))
      {  System.out.println("Error15: " + s + " should be 0/1");
      }     

      f = new Fraction(-0);
      s = f.toString();
      if (! (s.equals("0/1")))
      {  System.out.println("Error20: " + s + " should be 0/1");
      }

      f = new Fraction(2, 3);
      s = f.toString();
      if (! (s.equals("2/3")))
      {  System.out.println("Error25: " + s + " should be 2/3");
      }

      // A further 21 test cases
      // 3/4, -2/3, -3/4, 5/4, -5/4, 4/6, -4/6, 6/4, -6/4,
      // 3/1, -3/1, 0/1, 3/3, -3/3, 1/1, -1/1, 0/3, -0/1, -0/3, 2/-3, -2/-3
      // Maybe 4 more if you think you should test things precluded
      // by preconditions: 1/0, 3/0, -3/0, 0/0

      f = new Fraction(0, 1);
      if (! f.isZero())
      {  System.out.println("Error30: " + f + " not recognised as zero");
      }

      // 4 more test cases at least

      f = new Fraction(3, 1);
      if (! f.isInteger())
      {  System.out.println("Error35: " + f + " not recognised as integer");
      }

      // 6 more test cases at least

      Fraction f1;
      Fraction f2;
      f1 = new Fraction(2, 3);
      f2 = new Fraction(2, 3);
      if (! f1.equals(f2))
      {  System.out.println("Error40: " + f1 + " and " + f2 +
            " not recognised as equal");
      }

      // 2 more test cases at least

      Fraction f3;
      f1 = new Fraction(1, 2);
      f2 = new Fraction(1, 3);
      f3 = new Fraction(5, 6);
      if (! (f1.plus(f2).equals(f3)))
      {  System.out.println("Error45: " + f1 + " plus " + f2 +
            " not recognised as " + f3);
      }

      // Several more test cases at least

      f1 = new Fraction(1, 2);
      f2 = new Fraction(1, 3);
      f3 = new Fraction(1, 6);
      if (! (f1.minus(f2).equals(f3)))
      {  System.out.println("Error50: " + f1 + " minus " + f2 +
            " not recognised as " + f3);
      }

      // Several more test cases at least

      f1 = new Fraction(2, 3);
      f2 = new Fraction(1, 5);
      if (! (f1.plus(f2).minus(f1).equals(f2)))
      {  System.out.println("Error55: " + f1 + " plus " + f2 + " minus " +
            f1 + " not recognised as" + f2);
      }

      // Several more test cases at least

      for (int i = 0; i < 20; i++)
      {  int r1 = (int) (Math.random() * 101 - 50);
         int r2 = (int) (Math.random() * 101 - 50);
         int r3 = (int) (Math.random() * 101 - 50);
         int r4 = (int) (Math.random() * 101 - 50);
         if (r2 != 0 && r4 != 0)
         {  f1 = new Fraction(r1, r2);
            f2 = new Fraction(r3, r4);
            if (! (f1.plus(f2).minus(f1).equals(f2)))
            {  System.out.println("Error60 (random): " + f1 + " plus " + 
                  f2 + " minus " + f1 + " not recognised as" + f2);
            }
         }
      }
   }
}
