EECS 280 Project 5: Rational Number Calculator

advertisement
EECS 280 Project 5:
Rational Number Calculator
This project will give you experience implementing a container class, List, that is similar to the linked
list discussed in the lecture, but with a few differences: it is doubly-linked to allow efficient inserts and
deletes anywhere in the list and it supports an iterator.
You will then use your List to implement a Stack, which only allows push and pop operations from one
end. This Stack class makes use of the List class, so you should implement Listclass first.
Next, you will implement a Rational number class that allows you to represent fractions. The Rational
class is independent of other classes. So, if you prefer, you can implement and test it first.
Finally, you will use the Rational and Stack classes to implement a postfix (also known as RPN)
calculator. In a postfix calculator, operators appear after their operands, rather than in between them.
For example, to compute (2 + 3) * 5, you would type 2 3 + 5 *
List class
The methods you have to implement are given in List.h.starter. You should copy that file to List.h and then
implement each member function. The main difference from the version in lecture is that it is
doubly-linked and also allows you to create an iterator and then use the iterator to search, insert, or
delete at any position in the list. Note that this class is templated, so that it can hold data of any type. For
classes that use templates, it is necessary to give the code for member functions inside the header file (it
turns out that the compiler requires that in order to instantiate the class, given a specific type).
Therefore, there will not be a List.cpp. See the lecture slides on how to add member functions in the
header file for a template class.
You must not change the public interface of List class and you must use a doubly-linked list (i.e., nodes
chained using pointers) implementation (no arrays or vectors, etc.). The basic methods that List provides
are in List.h.starter.
You must manage memory allocation so that there are no memory leaks, no dangling pointers, etc. For
example, when adding an item, you will need to allocate the memory for a node to hold the item’s value
and the pointer to the next node in the linked list. When removing items, you will need to delete that
previously allocated memory. The destructor needs to ensure that all the nodes in the linked list are
deleted.
Write a unit test for your List in List_test00.cpp and compile and run it it using this command:
% g++ ­Wall ­Werror ­pedantic ­O1 List_test00.cpp –o List_test00
% ./List_test00
Or, simply type:
% make List_test00
% ./List_test00
Also, you can run all the tests at once like this:
% make test
Your unit test should return 0 if it passes and non-zero (using assert) if it fails.
Testing this class thoroughly using unit tests is important since not all of its functionality is used in the
calculator. A few examples of functionality that the calculator may fail to fully exercise include:
●
●
erase and insert via the iterator.
copy constructor and assignment operator.
Make sure you test for all the functionality, even if it is not important for getting the calculator to work.
We will test your List.h with unit tests.
Stack class
You should complete the implementation of List (and test it) before working on Stack. The skeleton code
for Stack is given in Stack.h.starter. Copy Stack.h.starter to Stack.h. You must only use the public interface
of the List class to implement the stack. The List class should not have any friends.
The core functions of the Stack class are push(item), pop(), and top(). See the RMEs for the description of
these operations. Given the List type, the basic operations on a stack, push() and pop(), are
straightforward to implement. To push, you can simply add an element at one end of the list (either end
will do). To pop, you simply remove the element from the same end of the list. Another function is top()
that simply returns the top element (as a constant reference to eliminate an unnecessary copy) without
modifying the stack.
The count_if member function (see Stack.h.starter) may require some explanation. We illustrate the
use of this with an example. Let’s say the Stack S holds integers and contains the following elements:
Stack<int> S;
S.push(2);
S.push(3);
S.push(4);
S.push(6);
S.push(2);
// S now holds 2 6 4 3 2
The count_if method allows you to count the number of elements that meet a certain predicate. For
example, isEven is a predicate functor that tells you if a number is even:
class isEven {
public:
bool operator() (int i) {
return (i % 2) == 0;
}
};
Now, we can call count_if to find the number of even items in the stack:
S.count_if(isEven()); //returns 4
We can automatically check the answer like this:
assert(S.count_if(isEven()) == 4);
To test your stack, write unit tests in Stack_test00.cpp and compile and run them like this:
% g++ ­Wall ­Werror ­pedantic ­O1 Stack_test00.cpp –o Stack_test00
% ./Stack_test00
Alternatively, you can use the given Makefile as follows:
% make Stack_test00
% ./Stack_test00
Also, you can run all the tests at once like this:
% make test
Rational Class
The Rational Class’s interface is given in Rational.h. You should write the code in Rational.cpp. Recall from
arithmetic that a rational number is simply one that can be represented as a ratio of two integers
(positive or negative). The following are all rational numbers:
1, 2, 2/5, 4/3, 0, -1/2, etc.
2/4 is also a rational number, but is equivalent to 1/2. Note that -0 is equivalent to 0.
Note that rational division is different from integer division.
10/11 * 11 gives 10/1 or simply 10.
We will also allow consider numbers in which the denominator is 0 to be Rational numbers, e.g.,
1/0 : infinity
-1/0 : -infinity
0/0 : undefined
4/0: equivalent to 1/0
-4/0: equivalent to -1/0
-0/0: equivalent to 0/0
(One of the reasons for allowing these forms of rational numbers, besides making the Rational class
more general, is so that you don't have to do error checking in the code for things like divide by 0, etc.)
Computers do not store fractions as ratio of two values. Instead, they store them as decimals. This can
cause some problems. For example, (1.0/3)*3.0 may not be exactly equal to 1 because of precision
errors inside the computer. The Rational class that stores both the numerator and denominator as
integers to represent a rational value will not have such a problem.
In the given Rational.h, you see two private data members: p and q. You should set p and q so that they
represent the rational number p/q. For example, if p is 1 and q is 2, then the given Rational object
represents a rational number 1/2.
Here is how to use the class to create some rational numbers:
Rational r(1, 2);
Creates a rational number 1/2
Rational r(2, 4);
Another way to create a rational number 1/2
Rational r(2, 1);
Creates a rational number 2/1
Rational r(2);
Another way to create a rational number 2/1
Rational r(0);
Creates a rational number 0/1
Rational r(­0);
Another way to create a rational number 0/1
Rational r();
Another way to create a rational number 0/1
For additional examples, use this website to test your rational number constructor.
●
●
●
●
●
●
●
Rational r(1, 2);
Rational r(2, 4);
Rational r(2, 1);
Rational r(2);
Rational r(0);
Rational r(-0);
Rational r();
// creates a rational number 1/2
// Also creates a rational number 1/2, since 2/4 is same as 1/2
// Creates a rational number 2/1 or simply 2.
// Another way to create a rational number 2/1 or simply 2.
// Create the rational number 0/1 or 0.
// Also creates the rational number 0/1 or 0. Note that -0 is equivalent to 0.
// Another way to create the rational number with value 0
Once you have rational numbers, we need to define operators, +, -, *, /, and == to manipulate or
compare them. You should implement these in Rational.cpp as non-member functions. The signature of,
say, + is given in Rational.h as follows:
// EFFECTS: Returns te lhs + rhs. Note that (a/b + c/d) is algebraically
// equivalent to (ad+bc)/bd.
Rational operator+(const Rational &lhs, const Rational &rhs);
When initializing the p and q values of a rational number and when updating them, you must reduce p and
q by dividing them by any common factor so that they are as small integers as possible. That is important
because if you don’t reduce them, you could run into integer overflows and incorrect results when none
should occur. For example, consider:
3/4 * 1000/1000 * 1000/1000 * [keep multiplying by 1000/1000 many times]
The above should result in3/4 for a correct implementation. But, if both numerator and the denominator
overflow, it is possible that you could get a different result. We do not want that. We will never test your
code with inputs where the results overflow (or even where the intermediate results in adding,
multiplying, etc. overflow). But, we will test for the above type of situation where the overflow is an
artifact of your implementation.
We have given you the following representation invariants for the internal values of integers pand qto
avoid unnecessary overflows and generate results that are consistent with our tests:
●
●
Invariant #1: Keep qas always greater than or equal to 0. pcan only be negative, 0, or positive.
Invariant #2: For non-zero p and q, p and q should not have a common factor other than 1.
Eliminate any common factors. When p is 0, and q is non-zero, set q to 1.
● Invariant #3: We want you to allow q to become 0, rather than treat it as an error. This could
result in three types of values: +infinity, -infinity, and undefined.
o Positive infinity is any positive number divided by 0. Internally, it must always be stored
with pbeing 1 and qbeing 0. When printed out, it should be printed out as 1/0.
o Negative infinity is any negative number divided by 0. Internally, it must always be
stored with pbeing -1 and qbeing 0. When printed out, it should be printed out as
-1/0.
o An undefined value is 0/0. Internally, it is simply represented with with pbeing 0 and q
being 0. When printed out, it should be printed out as 0/0.
For example, Rational(4,0) should be stored as Rational(1, 0), Rational(-4, 0) should be stored as
Rational(-1, 0). For example: all the following arithmetic operations are legal:
● 4/(2-2): results in +infinity, i.e., 1/0
● 4/-(2-2): also results in +infinity, i.e., 1/0. Note that -0 is same as 0.
● -4/(2-2) results in -negative, i.e., -1/0
● (2-2)/(3-3) results in an undefined value, i.e., 0/0
Remember that these invariants must hold after the Rational object is constructed and throughout its
lifetime, but they do not apply to the two parameters passed in to the constructor. The parameters
passed into the constructor can be arbitrary integers and the constructor should always succeed.
Accordingly, your implementation of the constructor must manipulate p/q based on equivalences (e.g.
reducing the fraction, moving or canceling the negative signs) to satisfy the invariants.
You will need to find a way to identify the largest common factor between the numerator and
denominator. For example, in 12/18, the largest common factor is 6. You should divide both numerator
and denominator by 6, resulting in 2/3. Thus, Rational(12, 18) should result in p as 2 and q as 3. You can
use a brute-force algorithm to find the largest common factor or use a well-known Euclid’s algorithm to
find that. Just make sure that you handle cases where the numerator and/or denominator is negative as
well. This could be the hardest part of implementing and testing the Rational class. There may be several
corner cases, such as negative values, 0 values, etc. You may have implemented the Euclid's algorithm
before (e.g., in a lab).
You also need to implement the method for printing the value of a rational number to a stream. When
the rational number is an integer, you should simply print the integer value. Otherwise, you should print it
as p/q, where any common factors have been eliminated. If the ratio is negative, the fraction will have –
sign since p will be negative. The signature of this method is given in Rational.h.
Finally, the arithmetic laws for rational numbers are as follows:
(a/b) + (c/d) = (ad + bc)/bd
(a/b) - (c/d) = (ad-bc)/bd
(a/b) * (c/d) = ac/bd
(a/b) / (c/d) = ad/bc
Assume that the above formulas also work when infinities and undefined values are encountered. For
example,
●
●
●
●
●
undefined + anything will result in undefined
infinity * 0 will result in undefined
infinity - infinity will result in undefined
2*infinity will result in infinity (i.e., 1/0)
2*infinity - infinity will result in undefined. Reasoning:
○ 2*1/0 will result in 1/0 (after multiplying and reducing to bring the result in compliance
with the invariants). Then, 1/0 - 1/0 will give 0/0 by applying the algebraic laws.
To test your rational class, you can use these commands:
% g++ ­Wall ­Werror ­pedantic ­O1 Rational_test00.cpp Rational.cpp ­o
Rational_test00
% ./Rational_test00
Or you can use the given Makefile:
% make Rational_test00
% Rational_test00
Also, you can run all the tests at once like this:
% make test
Postfix (RPN) Rational Number Calculator
You will now use your Stack and Rational classes to develop a Reverse-Polish Notation calculator for
rational numbers in calc.cpp.. Since all your classes for List and Stack are templated, it will be trivial
to create a calculator that works for rationals (it is also trivial to make it work for integers and doubles,
but you don’t have to implement them).
An RPN calculator is one in which the operators appear after their respective operands, rather than in
between them. So, instead of computing the following:
(( 2 - 3) * 4 )/ (-6)
an RPN calculator would compute this equivalent expression:
2 3 ­ 4 * ­6 /
RPN notation is convenient for several reasons. First, no parentheses are necessary since the
computation is always unambiguous. Second, such a calculator is easy to implement given a stack. In the
above case, when you see a number, you simply push it on the stack. When you see an operator, you
pop the top two values, apply the operator on them, and then push the result back on the stack. In the
above case, the stack would change as follows (top value shown first):
● Stack after seeing 2: [2]
● Stack after seeing 3: [ 3, 2]
● Stack after seeing - operator: [-1]
● Stack after seeing 4: [4, -1]
● Stack after seeing * operator: [-4]
● Stack after seeing -6: [-6, -4]
● Stack after seeing / operator: [2/3]
Notice that the stack only contains numbers at all times. The operators never go on the stack. Also note
that the stack contains a rational number equivalent to 2/3 at the end, which is internally represented as
a (p, q) pair (2,3) in your Rational class.
The above stack contents are a simplified view of what really is stored on the stack. At all times, the
stack only contains rational numbers. For example, after the first 2, the stack contains the object
Rational(2). After 3 is pushed, it contains [Rational(3), Rational(2)], etc. After the entire expression is
processed, the stack contains Rational(2, 3). All the arithmetic operators do rational arithmetic. Here is
another example:
Expression entered:
8 0 /
The final result on the stack will be equivalent to [Rational(1, 0)], since 8/0 should be reduced to 1/0 by
the invariant rules for rational numbers. This is same as infinity.
The calculator program is invoked with no arguments, and starts out with an empty stack. It takes its
input from the standard input stream, and writes its output to the standard output stream. Here are the
commands your calculator must respond to and what you must do for each. Each command is
separated from the next one by one or more whitespace characters (including possibly newlines).
<some number>
a number has the form: an optional - sign followed by one or more digits [0 – 9].
Thus, -2, 0, 23, 41, -41, -0 are all numbers. Notice that all these are integer values.
Push the value on the stack. Your calculator should convert these values to the
appropriate type (in this case, Rational, since you are implementing a rational
calculator) before pushing them on the stack. That ensure that your stack only has
to store one type of value, Rationals, and all operations are done only on rational
numbers.
The following are examples of things that are not valid numbers for user input: 0.3,
10/13, -1.1, 1,234. Note that rational values are never entered directly by the
user. Only integers are entered (to simplify your project). But, rational numbers will
result from the calculations and when values are pushed on the stack.
+
pop the top two numbers off the stack, add them together, and push the result onto the top
of the stack. This requires a stack with at least two operands.
­
pop the top two numbers off the stack, subtract the first number popped from the second,
and push the result onto the top of the stack. This requires a stack with at least two
operands.
*
pop the top two numbers off the stack, multiply them together, and push the result onto the
top of the stack. This requires a stack with at least two operands.
/
pop the top two numbers off the stack, divide the second value popped number by the first,
and push the result onto the top of the stack. This requires a stack with at least two
operands. the Rational class will provide the definition of /.
d
duplicate: pop the top item off the stack and push two copies of the number onto the top of
the stack. This requires a stack with at least one operand.
r
reverse: pop the top two items off the stack, push the first popped item onto the top of the
stack and then the push the second item onto the top of the stack (this just reverses the
order of the top two items on the stack). This requires a stack with at least two operands.
p
print: print the top item on the stack to the standard output, followed by a newline. This
requires a stack with at least one operand and leaves the stack unchanged.
c
clear: pop all items from the stack. This input is always valid.
a
print-all: print all items on the stack in one line, from top-most to bottom-most, each value
followed by a single space. The end of the output must be followed by exactly one newline.
This input is always valid and leaves the stack unchanged. For an empty stack, for example,
only the newline will be printed. As another example, For a stack with two elements, say
with stack contents being [Rational(10, 17), Rational(4)] (here, 10/17 is on top), the
following will be printed:
10/17 4 <NEWLINE>
(Where <NEWLINE> corresponds to the newline character produced by endl or "\n").
m
match: pop the last element off the stack. Then push back the number of times this element
occurs in the remaining stack. This requires at least one operand. Here is an example of
commands, starting with an empty stack:
223242m
The above should result in a stack with (top value shown first):
[3, 4, 2, 3, 2, 2]
The 2 that precedes m was popped off the stack. Then, we want to count how many values are
equal to this value in the remaining stack. There are three 2’s. Thus, we push 3 on the stack.
Here is another example of commands, starting with an empty stack:
2m
The above simply results in a stack with:
[0]
The reason is that there are zero values on the stack that match 2, once 2 is popped off. So, 0 is
pushed.
This one is the trickiest to implement. You are not allowed to actually pop all the elements
off the stack to implement this (you can only pop the top value). Instead, you must use
the count_if function that Stack class provides and push the final result. Functors or function
pointers may be useful here. Think about which one will work and then use it with count_if.
q
quit: exit the calculator with a 0 exit value. This input is always valid. End-of-file (e.g., typing
control-D on Linux) also must exit the calculator with a 0 exit value. Note: do not call exit(0)
because it will cause memory leaks!
Each command is separated by whitespace.
For simplicity, you can assume that you are given valid input in our tests. No error checking on inputs to
the calculator is required.
Implement your calculator in a file called calc.cpp.
To compile your calculator, you must use:
g++ ­Wall ­Werror ­pedantic ­O1 calc.cpp Rational.cpp ­o calc
(To debug, replace ­O1with ­g)
To run your calculator interactively, you can use:
% calc
And then start typing the commands.
General Requirements
●
Enforcing the REQUIRES clauses: Unlike your previous assignments, we would like you to enforce
the REQUIRES clauses by using assert statements in all your functions. This is a good idea for
defensive programming and to catch bugs more easily. Since Calculator class builds on Stack,
which builds on List, it is a good idea for each class to enforce its REQUIRES classes so that any
misuse of the class can be detected.
●
Use string class instead of CStrings. We taught you CStrings earlier for Project 3 to teach you
details of pointers and pointer arithmetic. But, using CStrings is generally considered a bad idea,
when they can be avoided. C++ strings are a lot safer. Besides, they are simpler to use. For
example, instead of using strcmp(s1, s2) to compare strings, you can simply use (s1 == s2), since
the C++ string class overloads the == operator and defines its meaning. Also, C++ strings can
never overflow, unlike CStrings.
●
We will use valgrind to make sure that your code does not leak memory or result in dangling
pointers. Test your code with valgrind to make sure that none of your classes leak memory.
●
You must not use STL or other types containers in the submitted assignment (though you are free
to use them for private testing). Instead, use the List.h and Stack.h that you implement.
●
Fully implement the ListADT.
●
In your calc.cpp you may #include <iostream>, <string>, <cstdlib>,
<cassert>, <cmath>. No other system header files may be included, and you may not
make any call to any function in any other library.
●
As usual you may not use goto, nor may you use any any global variables that are not const.
Getting Started
The files for this project live in /afs/umich.edu/class/eecs280/proj5
List.h.starter
implementations. Copy this
Stack.h.starter
Rational.h
skeleton Listclass header file without function
file to List.hand then add your
function implementations.
Skeleton Stack class. Copy it to Stack.h and then add your function
implementations.
Rational header file. Do not modify it. You will write the code for
the functions in Rational.cpp. This is not a templated class, so the
code can go in a .cpp file.
We have also provided you with some test files to get you started. See the Makefilefor how to
compile and run the tests. Note that these tests are not exhaustive. You should run your own tests,
making sure you test for situations that are not covered by the published test cases. For example, the
published test case for List does not test iterators, copy constructor, assignment, etc. The given tests for
Stack do not completely test if_count, printing of a stack, etc. The tests for Rational and for the overall
calculator also only do very basic tests.
Submission
Submit these files to the autograder:
● List.h
● Stack.h
● Rational.cpp
● calc.cpp (this will contain a main function)
For autograding portion of the grade, we will be compiling your code with the given unit tests as well as
our own unit tests for the three classes. In addition, we will be doing blackbox testing of calc with
additional inputs to the calculator. Make sure at the least that the given tests work. Then, do additional
testing where you find gaps in the given tests.
Information required at the top of each file:
At the top of all the files you submit, remember to include the following information (there is an
additional element - see carefully below):
● your name and uniquename
● Your partner's name and uniquename
● Substantially identical submissions with respect to style? Yes or no. (put this information
only in calc.cpp). f you select yes, we will grade only one of the submissions for style (either you
or your partner's) and assign the same style points to both. If you don't give this information, we
will assume that the answer is yes. If you select no, we will grade you and your partner
separately. You may receive different style scores, even for identical submissions, since style
points are not auto-graded and have some inherent subjectivity. We will not accept requests to
regrade because of differences of style points between the two partners unless both partners
submit the request jointly. A regrade could raise or lower your style score.
Appendix A: what's in a typename?
You saw the use of typename for declaring templates. When compiling your project, you may get the
following kind of error:
./Stack.h:94:8: error: missing 'typename' prior to dependent type name 'List<T>::Iterator'
List<T>::Iterator i;
If you see an error message that talks about missing 'typename' prior to dependent type name, simply
stick in the keyword "typename" before the type declaration. In the above instance, it would become:
typename List<T>::Iterator i;
Same thing would apply if you declare a loop variable. For example:
for (List<T>::Iterator i; ….)
may need to become (if you get an error from the compiler):
for (typename List<T>::Iterator i; …)
Discussion of dependent types and why we have to insert the keyword typename with the version of
C++ compiler that we are using is beyond the scope of this course (the reason is quite subtle and deep).
If you want to see some explanation, see this article:
http://pages.cs.wisc.edu/~driscoll/typename.html
Acknowledgements
This project was written by Atul Prakash and Andrew DeOrio.
Download