University of Toronto
Department of Computer Science
CSC148H, Summer 2006

Assignment 1

Introduction

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).

Why?

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.

Part 1

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.

Evaluation Algorithm

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.

What you need to do

Class LLStack

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.

Token classes

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:

  1. tokens which represent a numerical value. You must create a class ValueToken with the following methods:

  2. tokens which represent operators, which are described by the abstract class BinaryOp (because we are concerned with operations that require two operands). BinaryOp.java is provided. You must implement classes AddOp, SubtractOp, MultiplyOp, DivideOp, and ExponentOp, which represent addition, subtraction, multiplication, division, and exponentiation respectively. All subclasses of BinaryOp must have the following methods:

Class PostfixCalculator

The primary class of Part 1 is PostfixCalculator. In addition to its constructor, it has one public method:

JUnit test classes

You must provide tests using JUnit for both LLStack and PostfixCalculator, in classes LLStackTest and PostfixCalculatorTest.

Part 2

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.

Evaluation Algorithm

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.

What you need to do

Class LLPeekableStack

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:

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.

Token classes

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:

  1. tokens which represent other allowed symbols, such as brackets "(", ")". You must provide classes OpenBracket and CloseBracket, which as subclasses of CalcToken, and have no additional methods.

  2. tokens which represent anything else, which are unrecognized and thus an error. You must provide class ErrorToken, with the following methods:

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,

  1. A.getPrecedence() == B.getPrecedence() if the respective operations are of equal precedence.
  2. A.getPrecedence() > B.getPrecedence() if A's operation occurs before B's operation (i.e. it is of higher precedence).

Class InfixCalculator

The primary class of this part of the assignment is InfixCalculator. In addition to its constructor, it has the following methods:

JUnit test classes

You must provide tests using JUnit for both LLPeekableStack and InfixCalculator, in classes LLPeekableStackTest and InfixCalculatorTest.

What to submit for Assignment 1

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.

What is NOT allowed

You may not use any arrays, or any classes from java.util, which includes the Java Collections such as List.

Testing hints

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.

Coding hints

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.

Style hints

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.

Marks breakdown

Automarking: 40%
Style and design: 20%
Documentation: 20%
Test cases: 20%