For this assignment, you will write classes to evaluate arithmetic expressions represented as text. For example, the string "1 + 2 + (3 * 4)" would evaluate to 15. This process will be similar to how Java, and other programming languages, compile human-written code into machine instructions. Recall the terms syntax and symantics from CSC108 - compilation takes syntactic information (your code, essentially a long string of characters) and turns it into symantic information (that is, the operations you want the machine to perform).
You can write better code, and debug code more efficiently, if you understand how a compiler operates. In addition, it is helpful to learn the "lower-level" data structures like linked lists, and simple but fundamental components such as stacks. The assignment will also give you practice working with specifications, testing and documentation.
In order for all this to succeed, you must follow the specifications precisely. It is common even for quite experienced programmers to find that they have neglected some point of a specification and that their "obviously correct" program does not work properly. For beginners, reading carefully does not come naturally, and one thing you will learn from this assignment is that you do have to read parts of the handout more than once.
You will also learn that specifications never achieve complete precision; sometimes you'll need to ask us for further details, and sometimes you'll need to make an intelligent extrapolation on your own.
This assignment will also extend your experience with good programming practices, in the form of JUnit testing and Javadoc documentation.
Arithmetic expressions can be written in a variety of notations. Our normal notation is called Infix Notation because the operators (+, -, etc.) appear within their operands (numbers and variables). For example, "1 + 2". For this part of the assignment, we will use "Reverse Polish Notation", or Postfix Notation, where the operator appears after the operands. The previous infix example converted to postfix would be "1 2 +". Some examples:
Infix expression | Postfix expression |
---|---|
1 + 2 + 3 | 1 2 + 3 + |
6 * 5 + 4 | 6 5 * 4 + |
(7 + 2) * 4 | 7 2 + 4 * |
(4 + 3) - (2 * 7) | 4 3 + 2 7 * - |
Note that the results of operations can be operands to other operations - in the second example, "6 5 *" is the first operand for the "+" operator. In some infix examples, brackets/parentheses are used to "override" the usual order of operations; for example, 4+6*5 is not equal to (4+6)*5. However, note that in postfix notation, brackets are not needed - the order of operations is clear from the order of the operands and operators.
There is a very simple algorithm for evaluating syntactically correct postfix expressions, which reads the expression left to right in chunks, or tokens, which in this case are numbers or operators:
while tokens remain:
read the next token.
if the token is an operator:
pop the top two elements of the stack.
perform the operation on the elements.
push the result of the operation onto the stack.
else:
push the token (which must be a number) onto the stack.
When there are no tokens left, the stack only contains one item: the final value of the expression. For an example with stack diagrams, see the Wikipedia page on postfix notation.
The Stack interface that we used in lecture is provided in Stack.java. First, write a class called LLStack which implements the Stack interface using a linked list of your own creation. This may require additional classes or files. Because we are using a linked list instead of a fixed-size array, you may assume that memory is unlimited and new linked list nodes may always be created.
For this assignment, you don't have to worry about converting a string into a series of tokens. You are provided with class Tokenizer, which takes a String as input to its constructor. Tokenizer.java should not be modified, except to comment out the lines which require classes from Part 2, which are indicated in the code. You can use its hasMoreTokens() and nextToken() methods in your program to receive the tokens of the expression, one at a time. However, you must provide the token classes that Tokenizer uses, which are all subclasses of abstract class CalcToken. For this part of the assignment, there are two types of tokens:
ValueToken(double val)
where val is the number the token should represent.double getValue()
returns the value of the token.String toString()
which is inherited from the abstract parent class CalcToken.double doOperation(double left, double right)
returns the result of the operation using its left and right operands (note that operand order can matter, depending on operator).String toString()
which is inherited from CalcToken.The primary class of Part 1 is PostfixCalculator. In addition to its constructor, it has one public method:
double run(String expr)
where expr is a String representing a correct postfix expression, returns its
simplified numerical value. This method should use the algorithm described above, implemented using a LLStack.You must provide tests using JUnit for both LLStack and PostfixCalculator, in classes LLStackTest and PostfixCalculatorTest.
For the second part of the assignment, you will write classes to evaluate expressions in regular infix notation - even though we're more familiar with this format of writing expressions, they're harder to evaluate. In addition, you must also take into account bracketing used to override precedence (e.g. "(4+5)*6") as well as errors in the expression.
The algorithm we will use for evaluating infix expressions (which, I should note, is not the algorithm used by compilers) is similar to the algorithm from Part 1. However, because we now have bracket tokens and operator precedence, it is different. When considering a new token T, and the first three elements are (top-down) E0, E1, and E2, use the following rules:
E0 | E1 | E2 | Additional criteria | Action |
---|---|---|---|---|
ValueToken | BinaryOp | ValueToken | - T is a BinaryOp and E1 has equal to or higher precedence than T or - T is not a BinaryOp |
- pop E0, E1, E2 - push the value of E1 with operands E2 and E0 |
CloseBracket | Value | OpenBracket | - pop E0, E1, E2 - push E1 |
We will call this operation a collapse because it replaces three items on the stack with one simpler but equivalent item. Note that a collapse doesn't push a new token onto the stack (i.e. T ,above). Also note that multiple collapses are possible if a collapse creates the criteria for a subsequent collapse.
I've put up a step-by-step example of infix evaluation which may help.
Because evaluating infix expressions is more complex than with postfix, we require an augmented stack with some additional functionality. Specifically, we need to be able to "peek" into the stack and examine some of its elements without removing them from the stack. You are provided with the PeekableStack interface, which requires the following methods:
Object peek(int i)
returns the i-th stack element.int size()
returns the current size of the stack.void clear()
empties the stack.String stackContents()
returns a \n-delimited String
representing the stack's contents.This interface must be implemented in a class called LLPeekableStack, which (predictably) uses a linked list to hold the stack items. Again, you may assume there is unlimited memory and linked list nodes may always be created. As well, you may copy code from your LLStack class.
For this part of the assignment, you will be using the token classes which you previously created. In addition, Tokenizer requires two more types of tokens:
ErrorToken(String text)
, a new constructor which accepts the text containing the error.String toString()
, which returns the String "SYNTAX ERROR: blah", where "blah" was the error text provided with the constructor.As well, this part of the assignment uses the getPrecedence() method of abstract class BinaryOp. You must modify your token classes from Part 1 to ensure that for any two BinaryOp objects A and B,
The primary class of this part of the assignment is InfixCalculator. In addition to its constructor, it has the following methods:
boolean run(String expr)
processes expr using the above algorithm and a LLPeekableStack. Returns true
if expr is a valid infix expression and has been evaluated, and false
otherwise.boolean isOkay()
returns true
if the most recent call to run() provided a valid infix expression, and false
otherwise.double getResult()
returns the result of the most recent successful evaluation.void collapse(CalcToken token)
collapses the stack until a collapse rule cannot be applied, given the next token.You must provide tests using JUnit for both LLPeekableStack and InfixCalculator, in classes LLPeekableStackTest and InfixCalculatorTest.
There is no need to submit the provided files which are complete and are not modified, such as Stack.java.
For all your methods, there must be appropriate Javadoc documentation, as well as appropriate comments within the methods. Your marks will depend on documentation and other aspects of style, as well as the success or failure of your program code itself.
You may not use any arrays, or any classes from java.util
, which includes the Java Collections such as List.
You should test each public method, using extreme ("boundary") correct and incorrect values, where allowed by preconditions, as well as the obvious "middle of the road" values. Remember to choose efficient test cases. For example, testing PostfixCalculator.run() with strings "1 2 +", "3 7 +", and "21 4 +" is unnecessary since they test the exact same thing - in this case, addition of two numbers.
Test basic methods before complicated ones.
Test simple objects before objects that include or contain the simple objects.
Don't test too many things in a single test case.
Write your test cases early. It's easier to think of test cases when you can remember the troubles you had writing the code, and the process of writing and running tests can also help you to write the code if you do it early.
Write your Javadoc documentation before writing the method it describes. Change it if you realize you've misunderstood. Don't leave documentation to the end, when you no longer remember your code.
Make sure your class names are spelled exactly right, agreeing with the spelling in this handout - including capitalization - and make sure that your file names match the class names.
Start with the simple operations, then write the complex operations that depend on them. As well, Part 1 can be written before fully understanding Part 2.
The method PeekableStack.contents() can be useful for debugging, to see the contents of your entire stack.
Remember how the stack order of the tokens relates to the written order of the tokens.
The expectations in CSC 148H/A48H are much the same as in CSC 108H/A08H: comments explaining complicated code, well-chosen names for variables and methods, clear indentation and spacing, etc. In particular, we expect you to follow the capitalization conventions: variableName, methodName, ClassName, PUBLIC_STATIC_FINAL_NAME.
We are likely to use CheckStyle to look for stylistic difficulties in your code, and it would be sensible for you to do that yourself before submitting your work.
Automarking: 40%
Style and design: 20%
Documentation: 20%
Test cases: 20%