Session 11: Fraction class example

Design
Coding (Section 3.8)
Summary
Exercise

Design

Today we're going to look at an extended example of defining a class, to get a better feel for how the design process works. We'll define a class to represent a fraction. This class is actually included in the csbsju.cs160 library, almost exactly as presented here.

We begin our task by asking ourselves the question: What are the properties of a fraction? That is, what does an individual fraction need to ``remember''? In the case of a fraction, we reason, there are two properties of which we need to keep track: the numerator and the denominator. We're going to make the decision right now that we'll always store a fraction in reduced form - 2/4 will be remembered as 1/2, for example.

One decision that we'll make right now is that a fraction will be immutable - once created, its properties won't change. If we want a different fraction, then we'll have to create a brand-new one anew.

We also ask ourselves: What must a fraction ``do''? That is, what methods do we need to implement? There are a several things we might want to do to a fraction.

It's also useful to define two Fraction constants ZERO and ONE: Our reason for making these constants is similar to the red and black constants in the Color class: We will tend to want to refer to them often, and it would be nice if we don't have to create a new instance to do this.

Coding

Textbook: Section 3.8

Remember that a class definition is a grouping of four possible types of definitions: class variable definitions, instance variable definitions, instance method definitions, and class method definitions. (That's the order I typically would write them, too.)

Now we'll write our class. We begin by putting in our instance variables.

import csbsju.cs160.*;

public class Fraction {
    private int num;
    private int den;
    
    // ... rest of stuff will go here
}
We're defining two instance variables, called num and den. Both should be fractions.

Now we'll add our constructor method, which takes two parameters specifying the numerator and denominator.

    public Fraction(int numerator, int denominator) {
        num = numerator;
        den = denominator;
    }
This is a good first shot, but it isn't entirely accurate: We decided to remember fractions in lowest form, and the two parameters passed in won't necessarily be in lowest form yet. To do this, we'll want to compute the GCD of the numerator and denominator, and divide both numbers by the GCD.

To help compute the GCD, we'll add a new method. We don't technically need to define this method, but it helps makes things clearer to have this additional method. This method is a static method (since it doesn't operate directly on a fraction), and it's a private method (since it doesn't really belong in the Fraction class.

    public Fraction(int numerator, int denominator) {
        if(denominator < 0) {
            numerator *= -1;
            denominator *= -1;
        }

        int div = gcd(numerator, denominator);
        num = numerator / div;
        den = denominator / div;
    }

    private static int gcd(int a, int b) {
        while(b != 0) {
            int r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

The first method is simply to convert a fraction into its closest double approximation. This will be a matter of performing the division - but we'll need to be careful to do double division, not integer division.

    public double doubleValue() {
        return (num + 0.0) / den;
    }

Now we'll do the arithmetic methods. For the add method, we employ the following equation.

 a     c    ad + bc
--- + --- = -------
 b     d       bd
The add() method will just be a straightforward translation of this equation.
    public Fraction add(Fraction other) {
        return new Fraction(num * other.den + den * other.num,
            den * other.den);
    }
The multiply() method is even simpler.
    public Fraction multiply(Fraction other) {
        return new Fraction(num * other.num, den * other.den);
    }
I'm not showing you subtraction and division, but they're simple modifications of the above.

Now we want to define the compareTo() method. To do this, we observe that comparing a/b to c/d is the same as comparing a*d to b*c (assuming b and d are positive, something that we ensured in the creation of the Fraction).

    public int compareTo(Fraction other) {
        // first check if they are equal
        if(num == other.num && den == other.den) return 0;

        // otherwise see which is less
        int first = num * other.den;
        int second = other.num * den;
        if(first < second) return -1;
        else return 1;
    }

The next method we need is the toString() method.

    public String toString() {
        if(den == 1) {
            return "" + num;
        } else {
            return num + "/" + den;
        }
    }

That leaves the parseFraction class method. This is a class method since it isn't something we'd want to do on a particular Fraction object - it's just something we might reasonably ask the Fraction class to do for us.

    public static Fraction parseFraction(String str) {
        int numer; // numerator of new fraction
        int denom; // denominator of new fraction

        int slash = str.indexOf("/");
        if(slash < 0) { // the string contains only an integer
            numer = Integer.parseInt(str);
            denom = 1;
        } else {
            numer = Integer.parseInt(str.substring(0, slash));
            denom = Integer.parseInt(str.substring(slash + 1));
        }

        return new Fraction(numer, denom);
    }

Summary

This gives us the following complete class definition.

import csbsju.cs160.*;

public class Fraction {
    public static final Fraction ZERO = new Fraction(0, 1);
    public static final Fraction ONE = new Fraction(1, 1);

    private int num;
    private int den;

    public Fraction(int numerator, int denominator) {
        if(denominator < 0) {
            numerator *= -1;
            denominator *= -1;
        }

        int div = gcd(numerator, denominator);
        num = numerator / div;
        den = denominator / div;
    }

    private static int gcd(int a, int b) {
        while(b != 0) {
            int r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

    public double doubleValue() {
        return (num + 0.0) / den;
    }

    public Fraction add(Fraction other) {
        return new Fraction(num * other.den + den * other.num,
            den * other.den);
    }

    public Fraction multiply(Fraction other) {
        return new Fraction(num * other.num, den * other.den);
    }

    public int compareTo(Fraction other) {
        // first check if they are equal
        if(num == other.num && den == other.den) return 0;

        // otherwise see which is less
        int first = num * other.den;
        int second = other.num * den;
        if(first < second) return -1;
        else return 1;
    }

    public String toString() {
        if(den == 1) {
            return "" + num;
        } else {
            return num + "/" + den;
        }
    }

    public static Fraction parseFraction(String str) {
        int numer; // numerator of new fraction
        int denom; // denominator of new fraction

        int slash = str.indexOf("/");
        if(slash < 0) { // the string contains only an integer
            numer = Integer.parseInt(str);
            denom = 1;
        } else {
            numer = Integer.parseInt(str.substring(0, slash));
            denom = Integer.parseInt(str.substring(slash + 1));
        }

        return new Fraction(numer, denom);
    }
}

Exercise