CS 261 Assignment #6

Due Saturday, April 15th by 11:59pm
(Not accepted after 4/17)

Introduction

For this assignment you'll implement a class for representing and evaluating arithmetic expressions. It's a great excuse to use recursion, trees, sets, and maps, all within a single assignment! Even better — it happens to be useful. Make sure you write clean, well-documented code, since you'll be using this class as part of the next assignment as well.

Teams

You are required to work in pairs on this assignment. (Team assignments are below.) Do not start writing any code until you've met with your partner and discussed the assignment and possible approaches. As on the lab, please be kind in your interactions with your partners! Keep in mind that students in this class have a range of previous programming experience, and that some have been college students for longer than others. We're all in this together, and you have something to learn from your partner, no matter who they are or what their previous experiences have been. Ideally, your team would work together using the same pair programming approach as used on the lab, but however you choose to complete the assignment, I will assume that each member of the team has contributed equally to the project. If that assumption isn't true, please contact me privately.

Rohan Crossland and Alex Lopez
Spencer Racca-Gwozdzik and Brett Garon
Brendan Bell and Ella Slattery
Mackenzie Leibee and Noah Zimmer
Devan Meyer and Lucas Takiff
Gabriel Guinn and Noah McCullough
Bonacic Bonacic and Jacob Endrina
Lucas Calaff and Cian Monaghan
Asa Bonner and Grey Roppolo
Jinwoo Choi and Noah Sprenger
Stephen Rice and Ethan Spence

Expressions

The expressions we'll work with consist of the four binary operators (+, -, *, /), floating-point numeric literals (doubles), and variables (Java-like variable names). We'll also allow parentheses for grouping terms. Here are some examples: expression tree
3
2.17 + 5.8
x * 3.14159
(3.14 - 1) * 2.0
(10 + foo) * (30 - bar + 50)
Expressions like these can be represented by expression trees, in which the internal nodes contain operators, and the operands are at the leaves. The last expression above can be represented by the expression tree to the right.

Expression trees are a convenient way to represent arithmetic expressions since the structure of the tree encodes the order in which the operators should be applied — no parentheses are needed. Operators at higher levels in the tree are applied only after those at lower levels, so the multiplication at the top of the tree above is the last operator to be applied. Note that its right subtree correctly shows that the subtraction should occur before the addition (we take the minus and plus operators from left to right in the original expression).

They're also convenient because an expression in tree form can be easily evaluated, using recursion: The root can evaluate its left and right subtrees, then combine the results as appropriate. For example, the multiplication at the top of the tree would evaluate its left and right subtrees, then multiply the two results and return the product. The process is recursive since the addition in the left subtree similarly asks that its two subtrees be evaluated, then adds the results, etc. A representation of an expression tree as a String can be produced by an in-order traversal of the tree like the one we did in class.

Previously we saw how we could use a stack to evaluate an expression, but the plan here is to represent a formula that could potentially contain the names of variables, and re-evaluate the expression any time the variables change.

Expression Trees

One way to represent the tree shown in the previous section would be to use a BinaryTree of Strings: The root would contain "*", for example, and the leaves could be things like "10" and "foo". That would store all the necessary information, but using it during an evaluation would be difficult. We would have to parse a node's string to see what kind of information it contained, and then convert it to a numeric value or operator as necessary.

Instead, we'll create a custom tree implementation just for this project that uses different node classes for each of the three kinds of nodes in the tree: Nodes that represent constant values (e.g. 6), nodes representing variables (e.g. foo), and nodes representing arithmetic operators (e.g. +). Using a different node class for each kind of node also means that we can take an object-oriented approach to implementing the evaluation of a tree: Each node class can have a node-specific evaluate method! A numeric value node can just return its value when it's asked to evaluate itself, and a variable node can look up its current value in a Map and return it. An operator node can call evaluate on its left and right subtrees, then combine those values appropriately. For example, a + operator node would call evaluate on its left and its right subtrees, sum the values from those calls, and return the result.

Put another way, the nodes in our expression trees won't just store information, they'll do things as well. Each node in the tree will know how to evaluate itself. We'll make sure each kind of node implements toString as well, so that we can print trees. The node classes will each implement the Evaluable interface shown below, so that the code we write to use these expression trees is assured that every node supports the following operations:

package arithmeticExpression;

import java.util.Map;

public interface Evaluable {
    public double evaluate(Map<String, Double> env); 
    public String toString();
}

Classes implementing this Evaluable interface must have an evaluate method, and override the toString method. Do not modify the interface above to include any new methods. The different node classes will contain different fields — for example, operator nodes will need to contain references to their left and right subtrees, but value nodes won't since they don't have children. Regardless of the node type, each will have to implement evaluate in the appropriate manner: Value nodes can simply return their value, variable nodes will look up the variable's value in the Map that's passed to evaluate, and operator nodes will apply their operator to their subtrees' values. Each will also implement toString as appropriate (variables return their names, operator nodes return strings from their left subtree + operator + subtree, etc.). The output from toString should be a fully parenthesized expression, formatted such that it could be successfully passed to the constructor described in the next section.

The Expression class

We'll also write an Expression class to encapsulate our expression trees, just like the book writes a BinaryTree class to hide the details of their linked tree nodes. The Expression class will contain some methods that involve the entire tree, not just individual nodes, and will have a constructor that produces complete expression trees from arithmetic expressions passed in as strings. The documentation for the Expression class is here, and gives you the full list of methods the class must implement.

Building Trees from Strings

The constructor for the Expression class must build a tree from an infix expression string. This is a challenge, since the operator precedence and parenthesized subexpressions must be handled properly — you can't just process the operators in the order you find them in the input string. Luckily, you can stitch together some code and algorthms from Chapter 3 to make this easier.

First, within the constructor you can convert parenthesized infix expressions to postfix expression strings using the InfixToPostfixParens class from the book. (You do not need to understand how this code works.) This might not seem like a step in the right direction, but at least the parenthesized subexpressions will be processed away and, more importantly, the operators will now appear in the order in which they need to be applied. For example, the infix expression 5 * (1 + 2) - 6 would be transformed into the postfix expression 5 1 2 + * 6 -. In the original expression, the parentheses tell us that the + should be performed first, precedence says that the * is next, followed by the -. Lo and behold, that's the order in which they appear in the postfix string!

We can also borrow the stack-based algorithm we wrote in class for evaluating postfix expressions. It said we should push operands onto the stack until we came to an operator, then apply it to the top two items on the stack and push the result. You can use this same algorithm to build expression trees! Have the stack hold references to tree nodes: When you encounter a value or a variable, make a new tree node and push it onto the stack. When an operator is encountered, build a larger tree out of the operator and the two subtrees at the top of the stack, then push this new tree onto the stack. A reference to the full expression tree will be on the stack when we're done!

One word of warning though: The convert method in InfixToPostfixParens gets confused unless there are spaces separating the operators, operands, and parentheses. We'll place these same restrictions on the input to the Expression constructor. Thus, the expression above would actually have to be written as "5 * ( 1 + 2 ) - 6", with spaces around the parentheses.

Assignment Specifics

All code you write should be part of a package called arithmeticExpression. This will make it easier to plug this code in as part of your final assignment. Your project structure will look something like this:

Sample Interactions

Some sample interactions with the Expression class are shown below. Note that the last three demonstrate exceptions being thrown.

> Expression e = new Expression("5.2");
> e.toString()
   "5.2"   (String)
> e.evaluate(null)
   5.2   (double)
> e = new Expression("1 + 2 * 3 - 4");
> e.toString()
   "( ( 1.0 + ( 2.0 * 3.0 ) ) - 4.0 )"   (String)
> e.evaluate(null)
   3.0   (double)
> e = new Expression("1 + 2 * ( 3 - 4 )");
> e.toString()
   "( 1.0 + ( 2.0 * ( 3.0 - 4.0 ) ) )"   (String)
> e.evaluate(null)
   -1.0   (double)
> e = new Expression("1 + 5 / 3");
> e.toString()
   "( 1.0 + ( 5.0 / 3.0 ) )"   (String)
> e.evaluate(null)
   2.666666666666667   (double)
> e.getVariables()
   <object reference>   (HashSet<String>)
> e.getVariables().toString()
   "[]"   (String)
> import java.util.HashMap;
> HashMap env = new HashMap();
> env.put("foo", 17.8);
> e = new Expression("foo");
> e.evaluate(env)
   17.8   (double)
> e = new Expression("foo + 1");
> e.toString()
   "( foo + 1.0 )"   (String)
> e.evaluate(env)
   18.8   (double)
> e = new Expression("foo + 1 * ( foo + 2.5 )");
> e.toString()
   "( foo + ( 1.0 * ( foo + 2.5 ) ) )"   (String)
> e.evaluate(env)
   38.1   (double)
> e.getVariables().toString()
   "[foo]"   (String)
> e = new Expression("java + puppies / java * foo + 1.5");
> e.toString()
   "( ( java + ( ( puppies / java ) * foo ) ) + 1.5 )"   (String)
> e.getVariables().toString()
   "[java, foo, puppies]"   (String)
> e.evaluate(env)
Exception: java.lang.IllegalArgumentException (java)
> e.evaluate(null)
Exception: java.lang.IllegalArgumentException (java)
> e = new Expression("1 + 2 3");
Exception: arithmeticExpression.InfixToPostfixParens$SyntaxErrorException
    (Too many operands in input expression)

Asking for Help

You are more than welcome to ask for assistance during office hours, tutoring sessions, or via email, but when asking for help be prepared to answer the following questions:

Grading

This assignment will be graded out of a total of 100 points.

Submitting

Since your code was in a package called arithmeticExpression, it should be contained within a folder with that same name. Please zip up the arithmeticExpression folder containing your code and submit the .zip archive via Canvas as you've done previously.


Brad Richards, 2023