Previous Section
 < Free Open Study > 
Next Section


Case Study

Fraction Class

Problem Write and test a C++ class that represents a fraction.

Discussion A fraction is made up of a numerator and a denominator, so our fraction class must have data members for each of these. What operations do we normally apply to fractions? First we must initialize a fraction by storing values into the numerator and the denominator, and we need member functions that return the numerator and the denominator. Another operation would reduce the fraction to its lowest terms. We should also be able to test whether the fraction is equal to zero or greater than 1. If the fraction is greater than or equal to 1 (not a proper fraction), we should have an operation that converts the fraction to a whole number and a fraction. There are binary operations on fractions, but we are asked only to write and test a class that represents a fraction. Binary operations could be added later.

Let's summarize what we have said so far using a CRC card. A CRC card is a 4" x 6" or a 5" × 8" card on which we record the name of the class, the responsibilities, and the classes with which the class collaborates. CRC cards are used frequently in object-oriented design, and we discuss them in more detail in later chapters. Here we use one to record what we have decided our fraction class must do. We call the actions that the class must perform the responsibilities of the class. We use a handwriting font to indicate that CRC cards are a pencil and paper tool. We change to a monospaced font for the operations when we are talking about their implementation.

Click To expand

Before we translate this CRC card into a class definition in C++, let's examine each operation again. Let's change the expressions for the responsibilities into function names. The Initialize operation takes two integer values and stores them into the data members of the class. Let's call these data members num and denom. NumeratorIs and DenominatorIs return the values of the data members.

Reduce checks whether the numerator and the denominator have a common factor and, if they do, divides both by the common factor. On second thought, should making sure that the fraction is in reduced form be left to the user of the fraction class? If a fraction is not reduced to its lowest terms, binary arithmetic operations could cause overflow problems; the sizes of the numerator and denominator could become quite large. Let's remove this operation as a member function and make it a precondition for instances of our fraction class. If binary operations are added to the class, it becomes the responsibility of these operations to reduce the resulting fraction to its reduced form.

Iszero tests whether the fraction is zero. How do we represent zero as a fraction? The numerator is zero and the denominator is 1, so IsZero tests whether the numerator is zero. IsGreaterThanOrEqualToOne is too long an identifier. Let's call the operation that tests to see if the numerator is greater than or equal to the denominator IsNotProper. ConvertToProper returns the whole-number part and leaves the remaining part in the fraction.

We are now ready to write the class definition. We know what each operation should do. What about the preconditions for the operations? All fractions involved must be initialized before the member functions are called and must be in reduced form. ConvertToProper should be called only if the fraction is improper.

class FractionType
{
public:
  void Initialize(int numerator, int denominator):
  // Function: Initialize the fraction
  // Pre:  Numerator and denominator are in reduced form
  // Post: Fraction is initialized
  int NumeratorIs();
  // Function: Returns the value of the numerator
  // Pre:  Fraction has been initialized
  // Post: Numerator is returned
  int DenominatorIs();
  // Function: Returns the value of the denominator
  // Pre:  Fraction has been initialized
  // Post: Denominator is returned
  bool IsZero();
  // Function: Determines if fraction is zero
  // Pre:  Fraction has been initialized
  // Post: Returns true if numerator is zero, false otherwise
  bool IsNotProper();
  // Function: Determines if fraction is a proper fraction
  // Pre:  Fraction has been initialized
  // Post: Returns true if fraction is greater than or equal to 1; false
  //       otherwise
  int ConvertToProper();
  // Function: Converts the fraction to a whole number and a
  //  fractional part
  // Pre:  Fraction has been initialized, is in reduced form, and
  //       is not a proper fraction
  // Post: Returns whole number
  //       Remaining fraction is original fraction minus the
  //       whole number: fraction is in reduced form
private:
  int num;
  int denom;
};

Test Driver

At this stage, before we write any code for the member functions, we can write our test driver using the algorithm shown in the last section. Let's call the instance of the FractionType fraction. Here is the portion of the algorithm that we must write:

while...
  Execute the command by invoking the member function of the same name
  Print the results to the output file
...

We have six member functions to test. We can set up an if-then-else statement comparing the input operation to the member function names. When the name matches, the function is called and the result is written to the output file.

if (command is "Initialize")
     Read numerator
     Read denominator
     fraction.Initialize(numerator, denominator)
     Write on outFile "Numerator:", fraction.NumeratorIs()
        "Denominator:", fraction.DenominatorIs()
else if (command is "NumeratorIs")
     Write on outFile "Numerator:", fraction.NumeratorIs()
else if (command is "DenominatorIs")
     Write on outFile "Denominator:", fraction.DenominatorIs()
else if (command is "IsZero")
     if (fraction.IsZero)
          Write on outFile "Fraction is zero"
     else
         Write on outFile "Fraction is not zero"
else if (command is "IsNotProper")
    if (fraction.IsNotProper())
          Write on outFile "Fraction is improper"
    else
        Write on outFile "Fraction is proper"
else
    Write on outFile " Whole number is", (fraction.ConvertToProper())
    Write on outFile "Numerator:", fraction.NumeratorIs()
    "Denominator:", fraction.DenominatorIs()

The file containing the specification of class FractionType is in file "frac.h". Here are the pieces that must be added to the generalized test driver to test this class:

#include "frac.h"               // File containing the class to be tested

FractionType fraction;          // Declaration of FractionType object
while (command != "Quit")
{
  if (command == "Initialize")
  {
    int numerator, denominator;
    inFile  >> numerator;
    inFile  >> denominator;
    fraction.Initialize(numerator, denominator);
    outFile << "Numerator: "  << fraction.NumeratorIs()
      << " Denominator: " << fraction.DenominatorIs()
      << end1;
  )
  else if (command == "NumeratorIs")
    outFile << "Numerator: "  << fraction.NumeratorIs()
      << end1;
  else if (command == "DenominatorIs")
    outFile << "Denominator: " << fraction.DenominatorIs()
      << end1;
  else if (command == "IsZero")
    if (fraction.IsZero())
      outFile << "Fraction is zero " << end1;
    else
      outFile << "Fraction is not zero " << end1;
  else if (command == "IsNotProper")
    if (fraction.IsNotProper())
      outFile << "Fraction is improper " << end1;
    else
      outFile << "Fraction is proper " << end1;
  else
  {
    outFile << "Whole number is " << fraction.ConvertToProper()
      << end1;
    outFile <<  "Numerator: "  << fraction.NumeratorIs()
      <<   " Denominator: " << fraction.DenominatorIs()
      << end1:
  }

  :
}

Function Definitions

We have the test driver and the specification file containing the class. Now we must write the code for the function definitions and write and implement the test plan. The algorithms for the first five functions are so straightforward that they can be written with no further comment. The fifth function, ConvertToProper, must return the whole-number integer. It is extracted by taking the integer result of dividing the denominator into the numerator. The integer remainder becomes the numerator of the remaining fraction, and the denominator remains the same. If the numerator of the remaining fraction is zero, we must set the denominator to 1 to be consistent with the definition of a zero fraction.

// Implementation file for class FractionType
#include "frac.h"
void FractionType::Initialize(int numerator, int denominator)
// Function: Initialize the fraction
// Pre:  numerator and denominator are in reduced form
// Post: numerator is stored in num; denominator is stored in
//       denom
{
  num = numerator;
  denom = denominator;
}
int FractionType::NumeratorIs()
// Function: Returns the value of the numerator
// Pre:  Fraction has been initialized
// Post: numerator is returned
{
  return num;
}
int FractionType::DenominatorIs()
// Function: Returns the value of the denominator
// Pre:  Fraction has been initialized
// Post: denominator is returned
{
  return denom:
}

bool FractionType::IsZero()
// Function: Determines if fraction is zero
// Pre:  Fraction has been initialized
// Post: Returns true if numerator is zero; false otherwise
{
  return (num == 0);
}

bool FractionType::IsNotProper()
// Function: Determines if fraction is a proper fraction
// Pre:  Fraction has been initialized
// Post: Returns true if num is greater than or equal to denom; false
//       otherwise
{
  return (num >= denom);
}

int FractionType::ConvertToProper()
// Function: Converts the fraction to a whole number and a
//       fractional part
// Pre:  Fraction has been initialized, is in reduced form, and
//       is not a proper fraction
// Post: Returns num divided by denom
//       num is original num % denom; denom is not changed
{
  int result;
  result = num / denom;
  num == num % denom;
  if (num == 0)
    denom = 1;
  return result;
}

Test Plan

We have six member functions to test. Two of the six are Boolean functions, so we need two test cases for each. Here, then, is a test plan that has eight cases. Note that we have to initialize the fraction three times: once for a proper fraction, once for an improper fraction, and once for zero.

Operation to Be Tested and Description of Action

Input Values

Expected Output

Initialize

3, 4

Numerator: 3

   

Denominator: 4

IsZero

 

Fraction is not zero

IsNotProper

 

Fraction is proper

NumeratorIs

 

Numerator: 3

DenominatorIs

 

Denominator: 4

Initialize

4, 3

Numerator: 4

   

Denominator: 3

IsNotProper

 

Fraction is improper

ConvertToProper

 

Whole number is 1

   

Numerator: 1

   

Denominator: 3

Initialize

0, 1

Numerator: 0

   

Denominator: 1

IsZero

 

Fraction is zero

Here are the input file, the output file, and a screen shot from the run:

Input File

Output File

Initialize

Test_Run_for_FractionType

3

Numerator: 3 Denominator: 4

4

Fraction is not zero

IsZero

Fraction is proper

IsNotProper

Numerator: 3

NumeratorIs

Denominator: 4

DenominatorIs

Numerator: 4 Denominator: 3

Initialize

Fraction is improper

4

Whole number is 1

3

Numerator: 1 Denominator: 3

IsNotProper

Numerator: 0 Denominator: 1

ConvertToProper

Fraction is zero

Initialize

 

0

 

1

 

IsZero

 

Quit

 
Click To expand


Previous Section
 < Free Open Study > 
Next Section
Converted from CHM to HTML with chm2web Pro 2.85 (unicode)