WSU Tri-Cities CptS 360 (Spring, 2022)

Lab 4: The rational Package

This lab will introduce you to the style of writing C known as "OOC" -- "Object-Oriented C". Doing OOC means following a series of conventions that other more recent OO languages such as Java or C++ include as part of their syntax. You'll find that just by adhering to these conventions you can come close getting the advantages of OO without giving up features like quick compile time, comprehensible declarations, fast execution, and readable compiler error messages.

This page will be available as

https://www.tricity.wsu.edu/~bobl/cpts360/lab05_rational/writeup.html

Calling it up in a browser will allow you to cut-and-paste code in the following and save typing. You may also download whole files from the lab link on the course web page.

We'll start with basics of OOC: objects and their methods. We'll add more OO features like constructors/destructors, inheritance, and templates in a later lab, if time permits.

The rational Package

The lab objective is to create a package that implements a "class" Rational that does rational arithmetic in an OO way, providing a C API (Application Program Interface) for the user.

The Rational Class

The rational package consists of a header rational.h and a single source file rational.c (which you will create below). To use the package, the user #includes the header and links with the compiled source (either as a ".o" file or as part of a library).

Here's rational.h, which defines the Rational class:

typedef struct {
    int num;
    int denom;
} Rational;

// RTNL_BUF_SIZE is the size of a buffer needed to hold any
// "(num/denom)" string representation of a rational. The buffer must
// hold 2 signed decimals with parentheses and a slash. We allow 4
// digits for each byte of the two values and another 8 bytes for
// additional characters.
enum { RTNL_BUF_SIZE = 2 * 4*sizeof(int) + 8 };

extern Rational rtnl_add(Rational rtnl0, Rational rtnl1);
extern Rational rtnl_sub(Rational rtnl0, Rational rtnl1);
extern Rational rtnl_mul(Rational rtnl0, Rational rtnl1);
extern Rational rtnl_div(Rational rtnl0, Rational rtnl1);
extern Rational rtnl_init(int num, int denom);
extern Rational rtnl_ipow(Rational rtnl, int ipow);

extern char *rtnl_asStr(Rational rtnl, char buf[RTNL_BUF_SIZE]);

You may download a copy from the lab link on the course web page. You may consider this a firm specification: You are not allowed to change it in any way.

As you can see, in OOC, classes become C typedef structs. We could also implement them as just plain structs, but using typedef structs saves our writing the word struct in a lot of places. Rationals contain an int numerator and an int denominator.

This header also declares several functions which, together with the class, are the package's API. In OO parlance, these are "methods". You will implement each of them according to the following notes and in the following order, but the first method to implement, however, is internal to rational.c and not in the API.

rtnl_simplify()

This method has the prototype:

static Rational rtnl_simplify(Rational rtnl);

It's declared as static, which means that only other methods in rational.c can call it. This is intentional.

It returns a "simplified" form of any Rational: dividing the numerator and denominator by their greatest common denominator (GCD). (Don't confuse the "D" in "GCD" with the denominator of the rational number.) Find the GCD by using one of the oldest algorithms known, Euclid's algorithm. (It might be even older: There's evidence Euclid only copied it from an earlier work.) With a couple of tweaks for this algorithm, it has this pseudocode:

let a be the absolute value of the numerator
let b be the absolute value of the denominator
as long as b is not zero,
assign b to a temporary value tmp
assign the remainder of a divided by b to b
assign tmp to a
(a is the GCD)

Once it has the GCD, rtnl_simplify() should divide both numerator and denominator by it in-place.

rtnl_simplify() has another task: By convention, the denominator is always positive, so if the denominator is negative, the method should negate both it and the numerator.

To prevent a recursive stack overflow, rtnl_simplify() should not call rtnl_init() (see below).

It then returns the result.

rtnl_add()

Now you're ready to start implementing the API with rtnl_add(), which returns the result of adding its two Rational arguments. Remember that:

(a)/(b) + (c)/(d) = (ad + bc)/(bd)

so fill in a new Rational variable with this result. Then return the result of passing that variable to rtnl_simplify(). That will guarantee that the rtnl_add() result is always simplified and reduce the chance of overflow, which is what happens when the magnitude of either the numerator or denominator are too big to fit in an int.

rtnl_sub()

rtnl_sub() is identical to rtnl_add() except for one character. (Guess which one?) You can also implement rtnl_sub() by negating one numerator and returning the result of calling rtnl_add().

rtnl_mul()

rtnl_mul() is just like rtnl_add(), except that the formula is:

(a)/(b) × (c)/(d) = (ac)/(bd)

rtnl_div()

rtnl_div() is just like rtnl_mul(), except that a couple of values in the formula are swapped. You can also implement rtnl_div() by flipping one numerator/denominator pair and returning the result of rtnl_mul(). Again, your call.

rtnl_init()

This simple function is used to initialize a Rational, possibly in a declaration such as:

Rational r = rtnl_init(3, 5); // set r to 3/5

all it does is set the fields of a local Rational and return the rtnl_simplify() result.

rtnl_ipow()

This raises a rational number to an integer power, which may be positive, negative or zero. While you could do this by applying pow() from the math library, pow() is designed to work with floating point numbers, so the result is imprecise for large integers. Instead, do it the hard way with repeated rtnl_mul() calls. Remember to allow for negative exponents!

rtnl_asStr()

Converts a rational to a string of the form "( num / den )" (with no spaces, see the header file for an exact description) where num is the numerator and dem is the denominator as decimal integers. It uses buf to hold the string value and returns buf as well so that it may be used in expressions like:

char rtnlBuf[RTNL_BUF_SIZE];
...
printf("result: %s\n", rtnl_asStr(rtnl, rtnlBuf));

to print out rationals.

Test Program

Here is a test program rational_t.c for the rational package:

#include <stdio.h> // for printf()
#include <stdlib.h> // for exit()

#include "rational.h"


int main(int argc, char *argv[])
{
    int num = 17*258;
    int denom = 64*258;
    Rational rtnl0 = rtnl_init(42, 19);
    Rational rtnl1 = rtnl_init(22, 13);
    char buf0[RTNL_BUF_SIZE], buf1[RTNL_BUF_SIZE], buf2[RTNL_BUF_SIZE];
    int ipow;

    printf("simplification of %d/%d = %s  (should be 17/64)\n",
           num, denom, rtnl_asStr(rtnl_init(num, denom), buf0));
    printf("%s + %s = %s\n", rtnl_asStr(rtnl0, buf0), rtnl_asStr(rtnl1, buf1),
           rtnl_asStr(rtnl_add(rtnl0, rtnl1), buf2));
    printf("%s - %s = %s\n", rtnl_asStr(rtnl0, buf0), rtnl_asStr(rtnl1, buf1),
           rtnl_asStr(rtnl_sub(rtnl0, rtnl1), buf2));
    printf("%s * %s = %s\n", rtnl_asStr(rtnl0, buf0), rtnl_asStr(rtnl1, buf1),
           rtnl_asStr(rtnl_mul(rtnl0, rtnl1), buf2));
    printf("%s / %s = %s\n", rtnl_asStr(rtnl0, buf0), rtnl_asStr(rtnl1, buf1),
           rtnl_asStr(rtnl_div(rtnl0, rtnl1), buf2));
    printf("warning: integer overflow about to occur\n"
           "  watch for negative signs\n");
    for (ipow = 0; ipow < 10; ipow++) {
        printf("%s**%d = %s\n", rtnl_asStr(rtnl0, buf0), ipow,
               rtnl_asStr(rtnl_ipow(rtnl0, ipow), buf1));
    }
    exit(EXIT_SUCCESS);
}

You may download a copy from the lab link on the course web page.

Submission

Put the source file, rational.c, and README.txt (if you choose to have one) in a tarball and submit it via Blackboard.

No makefile is required to be submitted for this lab, as the grader will compile your code and link it with compiled test code (which you do not have access to).