In this lesson, we’ll improve the structured programming approach for a mixed fraction calculator by utilizing struct to group related data elements. By doing so, we aim to enhance code modularity and readability. This will result in a more effective and concise implementation, showcasing the benefits of structured programming with data type grouping in C++.

Case study: A mixed fraction arithmetic calculator

We’ll follow the same pattern that we did in our last case study lesson. We’ll build our code step by step, creating a deeper understanding of how the functionality of structures enables us to reduce code complexity. In this case study, we’ll go over the addition function of the mixed fraction case study, and the rest of the operations will be given to you as exercises. So, let’s start.

Finding the required memory

In the procedural programming implementation, we used three variables for storing the data for a single mixed fraction (whole number, numerator, and denominator), so there was a separate variable for each fraction element. Considering we needed three mixed fractions (left, right, and final), that made a total of nine variables that were being passed to the add() function. This would make the variable management and tracking hectic, and in larger codes where we might need, say, the sum of four mixed fractions, it could lead to potential logical errors, and the code itself would become difficult to understand.

Luckily, structures in C++ provide immunity to this problem. Instead of passing three separate variables to the add() function, we can now easily pass a struct containing these three variables as its members, simplifying our problem. The comparison is drawn below.

With primitive data types
int w; //whole number
int n; //numerator
int d; //denominator




With struct
struct MixedFraction
{
    int sign; // either will be +1 or -1
    int w; //whole number
    int n; //numerator
    int d; //denominator
};

By employing struct, we can create grouped data (commonly referred to as an object) for MixedFraction. This data type will encompass all three elements of a mixed fraction, along with an additional variable to represent its sign, resulting in a simplified and more efficient code implementation.

MixedFraction result = {}; //Final fraction that will store the result
MixedFraction L = {}; // Left fraction
MixedFraction R = {}; // Right fraction

In the example above, we made three MixedFraction variables for each mixed fraction for our arithmetic operations. Throughout the case study, L and R will represent the left and right fractions, while the result fraction will be used to store the arithmetic operation output.

Implementing the add() function

Technically, instead of nine int variables that we passed to the add() function in procedural programming (left and right fraction variables by value and the final fraction’s variables that stored the result of the arithmetic operation by reference method), we now just have to pass three MixedFraction variables from the previous widget, like this:

void add(MixedFraction L, MixedFraction R, MixedFraction &result)

It is a void type function that will update the result fraction as it is passed by reference. It's important to note that we chose to pass variables by reference because we had the limitation of returning more than one variable from a function. So, it only made sense that updating three variables must require each one of them to be passed by reference. However, we don’t have this limitation now, and we can go a step further to make our code more modular and easier to manage.

By using MixedFraction as the function data type, we enable it to return a structure variable, thus excluding the need to pass the third result fraction by reference.

With primitive data types
void add(int l_w, int l_n, int l_d, //left
         int r_w, int r_n, int r_d, //right
         int &f_w, int &f_n, int &f_d); //final
With struct
MixedFraction add(MixedFraction L, //left
                  MixedFraction R) //right

The flow of the add() function

Following the previous convention, we’ll give you an overview of the add() function. The purpose here is to help you understand how and what helper functions are used, in what order they are used, and the general changes we’re supposed to make as compared to procedural programming.

Press + to interact
MixedFraction add(MixedFraction L, MixedFraction R)
{
MixedFraction result{};
//Step 1: Convert to Improper Fraction
improperFraction(L);
improperFraction(R); // L.w and R.w are 0's now
//Step 2: Make d same, and attach the sign to numerator
sameDenominator(L,R);
//Step 3: Compute resulting fraction
result.n = L.n + R.n;
result.d = L.d;
computeSign(result);
//Step 4: Reduce the fraction (GCD)
int gcdRes = gcd(abs(result.n), result.d); // numerator could be -ve
result.n /= gcdRes;
result.d /= gcdRes;
//Step 5: Convert to Mixed Fraction & Print
MixedFraction(result);
return result;
}

Note: Notice how we defined the result structure variable within the scope of the function. This helps us eliminate the need to initialize it in the main() function and allows us to pass it to functions by reference, improving overall variable management.

Much cleaner than before, right? Let’s look at the coding details.

Step 1: Convert the mixed fraction into an improper fraction

With the use of structures, converting mixed fractions into improper fractions is much easier now.

Press + to interact
void improperFraction(MixedFraction &frac)
{
frac.n = (frac.d) * (frac.w) + (frac.n);
frac.w = 0; // frac.sign/frac.d are unchanged
}
  • Line 1: A fraction of the MixedFraction data type is passed by reference to the improperFraction() function, meaning the members of this fraction will be updated using the void function.

  • Line 3: The frac.n numerator of the frac fraction is updated by adding the product of the frac.d denominator and the whole number frac.w to the frac.n numerator. Since we have associated a separate structure member to the sign of the fraction (frac.sign in this case), we don’t need to compute a sign or take abs() of each member.

  • Line 4: The whole number of the fraction is set to 0 because there is no whole number in an improper fraction. Overall, fraction sign and denominator values remain the same.

Step 2: Making denominators the same

The next step involves making denominators the same for both fractions so they can be added together.

Press + to interact
void sameDenominator(MixedFraction &L, MixedFraction &R)
{
// 1: left n = left n * right d
L.n = L.n * R.d * L.sign; // attaching sign to numerator
// 2: left n = right n * left d
R.n = R.n * L.d * R.sign; // attaching sign to numerator
// 3: Denominator calulation
L.d = R.d = L.d * R.d;
L.sign = R.sign = 0; // removing sign, as it is not needed anymore
}
  • Line 1: The left and right fractions are passed by reference to the sameDenominator() function, which updates the numerators and denominators of both improper fractions, making their denominators the same.

  • Line 4: This updates the numerator of the left fraction L.n by multiplying L.n with the right fraction denominator R.d and the overall sign of the left fraction L.sign.

  • Line 6: This updates the numerator of the right fraction R.n by multiplying R.n with the left fraction denominator L.d and the overall sign of the right fraction R.sign.

  • Line 8: The left and right fraction denominators are both equal to the product with each other.

Note: Sign is now associated with the numerators of both fractions as we needed to add both of these components. We'll need to disassociate it and assign it back to the resulting fraction's result.sign sign member later on.

Step 3: Computing the resulting fraction

The next step involves adding both fractions. This is done by accessing the members of each structure’s variables and adding them together.

Press + to interact
result.n = L.n + R.n; // Resultant numerator
result.d = L.d; //Resultant denominator
computeSign(result); // Resultant sign

The output of the addition is stored in the result fraction.

  • Line 1: The final fraction numerator result.n is updated by adding the left and right fractions’ numerators.

  • Line 2: The final fraction denominator result.d equals either the left or right fraction’s denominator. Since both are equal, we can use either one. In this case, we have used L.d.

  • Line 3: A compute_sign() helper function is used, which computes and assigns the overall sign for the resulting fraction.

The code for the compute_sign() helper function is given below.

Press + to interact
void computeSign(MixedFraction & frac)
{
if (frac.n < 0)
frac.sign = -1;
else frac.sign = +1;
frac.n = abs(frac.n);
}

Unlike the computeSign() function in procedural programming, this function disassociates the sign from the numerator of the fraction and assigns it to the fraction member frac.sign (which, in this case, is result.sign).

  • Line 1: A fraction is passed by reference to the computeSign() function.

  • Lines 3–5: The fraction sign is set to -1 (negative) if the numerator value is less than 0. Otherwise, it’s set to +1 (positive).

  • Line 6: The fraction numerator value is set to absolute, removing any sign.

Step 4: Reduce the fraction (GCD)

The next step involves reducing the result fraction before it is converted back to a mixed fraction.

Press + to interact
int gcdRes = gcd(result.n, result.d); // numerator could be -ve
result.n /= gcdRes;
result.d /= gcdRes;
  • Line 1: The gcd() helper function is called, and the greater common devisor of the numerator and the denominator of the result fraction is computed.

  • Line 2: The numerator of the result.n fraction is reduced.

  • Line 3: The denominator of the result.d fraction is reduced.

Step 5: Convert the resulting improper fraction back to a mixed fraction

The last step involves converting the improper fraction back to mixed form. To do that, we will repeat the same steps that we did for procedural programming. This time, instead of variables, we will access and update the structure variable’s members.

Press + to interact
void mixedFraction(MixedFraction &frac)
// Convert to Mixed Fraction
frac.w = frac.n / frac.d;
frac.n = frac.n % frac.d;
}
  • Line 1: A fraction is passed by reference to the mixedFraction() function.

  • Line 3: The fraction’s whole number is updated by dividing its numerator by its denominator.

  • Line 4: The fraction’s numerator is updated by taking the remainder of its numerator with its denominator.

Printing the resulting mixed fraction

To print the resulting mixed fraction, we create a new helper function called print().

Press + to interact
void print(string msg, MixedFraction frac)
{
cout<< msg <<" = " ;
if(frac.sign==-1)
cout << "-";
cout<< frac.w << "<" << frac.n << "/" << frac.d << ">\n";
}

How exactly the code works is not important in the context of this lesson. So, we can just copy and paste this into our use case.

Complete code

This is what the complete working code looks like:

Press + to interact
#include <iostream>
using namespace std;
struct MixedFraction
{
int sign; // either will be +1 or -1
int w;
int n;
int d;
};
//GCD helper function
int gcd(int a, int b)
{
while (a % b != 0)
{
int r = a % b;
a = b;
b = r;
}
return b;
}
//Print helper function
void print(string msg, MixedFraction frac)
{
cout<< msg <<" = " ;
if(frac.sign==-1)
cout << "-";
cout<< frac.w << "<" << frac.n << "/" << frac.d << ">\n";
}
//Step 1: Convert to Improper Fractions
void improperFraction(MixedFraction &frac)
{
frac.n = (frac.d) * (frac.w) + (frac.n);
frac.w = 0; // frac.sign / frac.d are unchanged
}
//Step 2: Make the Improper Fractions' denominator same
void sameDenominator(MixedFraction &L, MixedFraction &R)
{
// 1: left n = left n * right d
L.n = L.n * R.d * L.sign;
// 2: left n = right n * left d
R.n = R.n * L.d * R.sign;
// 3: Denominator calulation
L.d = R.d = L.d * R.d;
}
void computeSign(MixedFraction & frac)
{
if (frac.n < 0)
frac.sign = -1;
else
frac.sign = +1;
frac.n = abs(frac.n);
frac.n = abs(frac.n);
}
//Step 5: Convert back to Mixed fraction
void mixedFraction(MixedFraction &frac) // assume frac is an improper fraction
{
// Convert to Mixed Fraction
frac.w = frac.n / frac.d;
frac.n = frac.n % frac.d;
}
//ADDITION OPERATION
MixedFraction add(MixedFraction L, MixedFraction R)
{
MixedFraction result{};
//Step 1: Convert to Improper Fraction
improperFraction(L);
improperFraction(R); // L.w and R.w are 0's now
//Step 2: Making d same
sameDenominator(L,R);
//Step 3: Compute resulting fraction
result.n = L.n + R.n;
result.d = L.d;
computeSign(result);
//Step 4: Reduce the fraction (GCD)
int res = gcd(result.n, result.d); // numerator could be -ve
result.n /= res;
result.d /= res;
//Step 5: Convert to Mixed Fraction & Print
mixedFraction(result);
return result;
}
int main()
{
MixedFraction result= {}; //resulting fraction
MixedFraction left = {-1, 2, 1, 2}; //left fraction
MixedFraction right = {+1, 0, 1, 3}; //right fraction
result = add(left,right);
print ("Left", left);
print ("Right", right);
print ("Left + Right", result);
return 0;
}

The code is much cleaner, easier to understand, and overall more efficient. Play with the above code and see how the addition function works.

Note: Someone might raise an objection, attributing the simplicity in the code to the introduction of an extra variable, sign, within the struct. The question that might arise is: why wasn't the same efficiency observed in the case of other primitive data types? However, it's essential to bear in mind that programmers typically aim to minimize the number of variables for ease of management. In contrast, with struct, data is already grouped, negating the need to handle multiple parameters. This advantage grants programmers the leverage to make more straightforward decisions, capitalizing on the power of struct's organized data, which we have successfully utilized in our implementation as well.