Student project in 2025
During the semester the students will (individually) working on a larger project. They will step by step create a class for Rational number and its test environment. For every time you have to create a small additional task built on the previous results. We publish the solution after the deadline with some delay.
Submit the solutions on Canvas copying only the source of the solution as text.
Task 1: Create a canonical format of a Rational number
As the first task, a Rational number will be represented by a pair of integers, defined as struct Rational {int num; int denom; }; in a canonical form, i.e., num and denom are irreducible and denom is positive. As a task, define a function canonicalQ(int,int) to return the Rational number in this canonical form.
The following is a program to test your functions (don’t upload this).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <vector>
#include <utility> // for std::pair
struct Rational
{
int num; // numerator
int denom; // denominator, should be always positive
};
Rational canonicalQ( int n, int d); // define this function
int main()
{
std::vector<std::pair<int,int>> test = {
{ 1, 2 },
{ 2, 4 },
{ -2, 4 },
{ -2, -4 },
{ 2, -4 },
{ 2, 3 },
{ 0, 2 },
{ 0, -2 },
{ 12345678, 12345679 }
};
Rational q{ 2, 3 }; // this is the way to create a Rational
for ( auto [a,b] : test )
{
Rational r = canonicalQ( a, b);
std::cout << a << "/" << b << " = "
<< r.num << '/' << r.denom << '\n';
}
}
// Expected output:
1/2 = 1/2
2/4 = 1/2
-2/4 = -1/2
-2/-4 = 1/2
2/-4 = -1/2
2/3 = 2/3
0/2 = 0/1
0/-2 = 0/1
12345678/12345679 = 12345678/12345679
We can assume, that the denominator will be never 0. Test your program for additional data (as I will do it). Variable q in the code above show a sample how to create a Reational number inside a function to return.
Submit your solution to Canvas by 23. March, Sunday, 23:59h, copying only the source of the canonicalQ() function into the textbox.
Solution
This is one possible solution. There might be many possible good solutions. We will use std::gcd for compute the greatest common divisor. This function is available since C++17 from the <numeric> header file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cassert> // for assert
#include <numeric> // for std::gcd
// create canonical format of Rational number
// denominator should be always positive
Rational canonicalQ( int n, int d)
{
assert( 0 != d );
if ( 0 == n ) return Rational{ 0, 1 };
int g = std::gcd( n, d);
if ( d < 0 )
{
n = -n;
d = -d;
}
return Rational{ n/g, d/g };
}
Task 2: Create the four basic operations on Rational
In the second step you have to implement the four basic operations on Rational numbers: addition, substraction, multiplication and division. We can assume that all parameters are in canonical format and for division, we can also assume that the divider is not zero.
#include <iostream>
#include <vector>
#include <utility> // for std::pair
#include <cassert> // for assert
#include <numeric> // for gcd
struct Rational
{
int num; // numerator
int denom; // denominator, should be always positive
};
// output function for a Rational number
std::ostream& operator<<(std::ostream& os, Rational r)
{
os << "( " << r.num << "/" << r.denom << " )";
return os;
}
Rational canonicalQ( int n, int d);
// this four functions should be implemented:
Rational add( Rational r1, Rational r2);
Rational sub( Rational r1, Rational r2);
Rational mul( Rational r1, Rational r2);
Rational div( Rational r1, Rational r2);
int main()
{
std::vector<std::vector<std::pair<int,int>>> test_input = {
{ { 1, 2 }, { 2, 4 } },
{ { -2, 4 }, { 2, -3 } },
{ { 0, 2 }, { 5, 8 } }
};
for ( auto v : test_input )
{
Rational r1 = canonicalQ( v[0].first, v[0].second);
Rational r2 = canonicalQ( v[1].first, v[1].second);
std::cout << r1 << " + " << r2 << " == " << add(r1,r2) << '\n';
std::cout << r1 << " - " << r2 << " == " << sub(r1,r2) << '\n';
std::cout << r1 << " * " << r2 << " == " << mul(r1,r2) << '\n';
std::cout << r1 << " / " << r2 << " == " << div(r1,r2) << '\n';
}
}
// create canonical format of Rational number
// denominator should be always positive
Rational canonicalQ( int n, int d)
{
assert( 0 != d );
if ( 0 == n ) return Rational{ 0, 1 };
int g = std::gcd( n, d);
if ( d < 0 )
{
n = -n;
d = -d;
}
return Rational{ n/g, d/g };
}
/* expected output:
( 1/2 ) + ( 1/2 ) == ( 1/1 )
( 1/2 ) - ( 1/2 ) == ( 0/1 )
( 1/2 ) * ( 1/2 ) == ( 1/4 )
( 1/2 ) / ( 1/2 ) == ( 1/1 )
( -1/2 ) + ( -2/3 ) == ( -7/6 )
( -1/2 ) - ( -2/3 ) == ( 1/6 )
( -1/2 ) * ( -2/3 ) == ( 1/3 )
( -1/2 ) / ( -2/3 ) == ( 3/4 )
( 0/1 ) + ( 5/8 ) == ( 5/8 )
( 0/1 ) - ( 5/8 ) == ( -5/8 )
( 0/1 ) * ( 5/8 ) == ( 0/1 )
( 0/1 ) / ( 5/8 ) == ( 0/1 )
*/Submit your solution to Canvas by 6th April, Sunday, 23:59h, copying only the source of the new functions into the textbox.
Solution
This is one possible solution. There might be many possible good solutions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Rational add( Rational r1, Rational r2)
{
return canonicalQ( r1.num*r2.denom + r1.denom*r2.num,
r1.denom*r2.denom);
}
Rational sub( Rational r1, Rational r2)
{
return canonicalQ( r1.num*r2.denom - r1.denom*r2.num,
r1.denom*r2.denom);
}
Rational mul( Rational r1, Rational r2)
{
return canonicalQ(r1.num*r2.num, r1.denom*r2.denom);
}
Rational div( Rational r1, Rational r2)
{
return canonicalQ(r1.num*r2.denom, r1.denom*r2.num);
}
Task 3: Create a Rational class
Write a Rational class based on the Task2. The class can be constructed
with zero, one or two parameters. The zero parameter constructor creates 0/1,
the one parameter constructor creates n/1, and the two parameter constructor
n/d.
Define the a+b, a-b, a*b, a/b and ~a operators (the last is the reciprocal).
Implement the class in two files: heap.h rational.h and heap.cpp
rational.cpp. Think for the header guards, possible const member functions,
etc.
Here is a main function to test your solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <vector>
#include <utility>
#include "rational.h"
int main()
{
std::vector<std::vector<std::pair<int,int>>> test_input = {
{ { 1, 2 }, { 2, 4 } },
{ { -2, 4 }, { 2, -3 } },
{ { 0, 2 }, { 5, 8 } }
};
for ( auto v : test_input )
{
Rational r1{ v[0].first, v[0].second };
Rational r2{ v[1].first, v[1].second };
std::cout << r1 << " + " << r2 << " == " << (r1+r2) << '\n';
std::cout << r1 << " - " << r2 << " == " << (r1-r2) << '\n';
std::cout << r1 << " * " << r2 << " == " << (r1*r2) << '\n';
std::cout << r1 << " / " << r2 << " == " << (r1/r2) << '\n';
}
Rational x{16,-12};
std::cout << "x{16/-12} == " << x.num() << "/" << x.denom() << '\n';
Rational y{7}; // Rational(7/1)
std::cout << "y{7} == " << y.num() << "/" << y.denom() << '\n';
Rational z; // Rational(0/1)
std::cout << "z == " << z.num() << "/" << z.denom() << '\n';
Rational v = x;
std::cout << "v == " << v << '\n';
std::cout << "~v == " << ~v << '\n'; // reciprocal
std::cout << "1 + ~v == " << (1 + ~v) << '\n';
std::cout << "(1 + ~v)/2 == " << ((1 + ~v) / 2) << '\n';
return 0;
}
/* expected output:
1/2 + 1/2 == 1/1
1/2 - 1/2 == 0/1
1/2 * 1/2 == 1/4
1/2 / 1/2 == 1/1
-1/2 + -2/3 == -7/6
-1/2 - -2/3 == 1/6
-1/2 * -2/3 == 1/3
-1/2 / -2/3 == 3/4
0/1 + 5/8 == 5/8
0/1 - 5/8 == -5/8
0/1 * 5/8 == 0/1
0/1 / 5/8 == 0/1
x{16/-12} == -4/3
y{7} == 7/1
z == 0/1
v == -4/3
~v == -3/4
1 + ~v == 1/4
(1 + ~v)/2 == 1/8
*/
When submit the solution, just copy the content of rational.h and rational.cpp separated by a “—————” line to the canvas. Submit your solution on Canvas by 22th April, Tuesday 23:59.
Solution
Here is the rational.h. The constructor has default parameters, the canonicalQ method is used to create the canonical form. We declare the operators as global/namespace functions so both parameters have the same conversion environment.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef RATIONAL_H
#define RATIONAL_H
#include <iosfwd>
class Rational
{
public:
Rational(int n = 0, int d = 1) { canonicalQ(n,d); }
int num() const { return num_; }
int denom() const { return denom_; }
private:
void canonicalQ(int n, int d); // set canonical form
int num_; // numerator
int denom_; // denominator, should be always positive
};
// output function
std::ostream& operator<<(std::ostream& os, const Rational& r);
// basic operators
Rational operator+( Rational r1, Rational r2);
Rational operator-( Rational r1, Rational r2);
Rational operator*( Rational r1, Rational r2);
Rational operator/( Rational r1, Rational r2);
Rational operator~( Rational r);
#endif // RATIONAL_H
Here is rational.cpp, where we implement the member and namespace functions declared in rational.h. Implementations are similar the one for Task2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream> // for ostream
#include <cassert> // for assert
#include <numeric> // for gcd
#include "rational.h"
void Rational::canonicalQ( int n, int d)
{
assert( 0 != d );
if ( 0 == n )
{
num_ = 0;
denom_ = 1;
return;
}
int g = std::gcd( n, d);
if ( d < 0 )
{
n = -n;
d = -d;
}
num_ = n/g;
denom_ = d/g;
}
std::ostream& operator<<(std::ostream& os, const Rational& r)
{
os << r.num() << "/" << r.denom();
return os;
}
Rational operator+( Rational r1, Rational r2)
{
return Rational{ r1.num()*r2.denom() + r1.denom()*r2.num(),
r1.denom()*r2.denom() };
}
Rational operator-( Rational r1, Rational r2)
{
return Rational{ r1.num()*r2.denom() - r1.denom()*r2.num(),
r1.denom()*r2.denom() };
}
Rational operator*( Rational r1, Rational r2)
{
return Rational{ r1.num()*r2.num(), r1.denom()*r2.denom() };
}
Rational operator/( Rational r1, Rational r2)
{
return Rational{ r1.num()*r2.denom(), r1.denom()*r2.num() };
}
Rational operator~( Rational r)
{
return Rational{ r.denom(), r.num() };
}
To use the Rational class one have to compile the rational.cpp and link to the main.cpp. Use the -c flag to compile only, and not to call the linker.
$ ls
main.cpp rational.cpp rational.h
$ g++ -Wall -Wextra -c main.cpp
$ g++ -Wall -Wextra -c rational.cpp
$ g++ main.o rational.o
$ ./a.out
1/2 + 1/2 == 1/1
1/2 - 1/2 == 0/1
1/2 * 1/2 == 1/4
1/2 / 1/2 == 1/1
-1/2 + -2/3 == -7/6
-1/2 - -2/3 == 1/6
-1/2 * -2/3 == 1/3
-1/2 / -2/3 == 3/4
0/1 + 5/8 == 5/8
0/1 - 5/8 == -5/8
0/1 * 5/8 == 0/1
0/1 / 5/8 == 0/1
x{16/-12} == -4/3
y{7} == 7/1
z == 0/1
v == -4/3
~v == -3/4
1 + ~v == 1/4
(1 + ~v)/2 == 1/8