Calculator

Helping a computer interpret a Mathematical expression is a derivation of FRQ3 that involves using ArrayLists, Stack, and Map. These are key data structures that exist in Java. To obtain a deeper understanding of ArrayList, or its List interface, it is to use it in many applications.

Operators and Precedence

To support mathematical expressions you need to define Unique List of operators that are supported by your Calculator.

// Helper definition to define operators, lookup in MAP are fast and easy O(1) versus ArrayList O(n)
private final Map<String, Integer> OPERATORS = new HashMap<>();
{
    // Map<"token", precedence>
    OPERATORS.put("*", 3);
    OPERATORS.put("/", 3);
    OPERATORS.put("%", 3);
    OPERATORS.put("+", 4);
    OPERATORS.put("-", 4);
}

Organizing an Expression

To support terms in mathematical expression you need to define symbols (other than operators) that help delineate the elements of an expression. Ultimately, the String expression will be broken in distinct elements in a List, we will call each element a Token.

  • FRQ3 calls these delimiters. Many encoded strings contain delimiters. A delimiter is a non-empty string that acts as a boundary between different parts of a larger string. The delimiters involved in this question occur in pairs that must be balanced,with each pair having an open delimiter and a close delimiter.

  • FRQ3 mentions expressions. Expressions in mathematics use open parentheses "(" and close parentheses ")" as delimiters. For each open parenthesis, there must be a matching close parenthesis.

    • (x + y) * 5 is a valid mathematical expression.
    • (x + (y) is NOT a valid mathematical expression because there are more open delimiters than close delimiters.
// Helper definition for supported separators
private final Map<String, Integer> SEPARATORS = new HashMap<>();
{
    // Map<"separator", not_used>
    SEPARATORS.put(" ", 0);
    SEPARATORS.put("(", 0);
    SEPARATORS.put(")", 0);
}

Expression Evaluation

To detect operators and separators in the String expression, requires test functions to detect the tokens. These methods will assist in establishing Control Flow in evaluating the expression.

// Test if token is an operator
private boolean isOperator(String token) {
    // find the token in the hash map
    return OPERATORS.containsKey(token);
}

// Test if token is an separator
private boolean isSeparator(String token) {
    // find the token in the hash map
    return SEPARATORS.containsKey(token);
}

// Compare precedence of operators.
private Boolean isPrecedent(String token1, String token2) {
    // token 1 is precedent if it is greater than token 2
    return (OPERATORS.get(token1) - OPERATORS.get(token2) >= 0) ;
}

Calculator Theory

Mathematical expression written by humans are in the form of a String Expression. Since this expression is simply a string to the computer... an algorithm is required to reform the equation for the computer to ensure it interprets expression according to the rules of mathematics.

  • In mathematics, an expression or mathematical expression is a finite combination of symbols that is well-formed (ie "100 + 200").
  • In computers, a string expression is hard to calculate. Thus, the expression needs to be restructured according to rules of Mathematics that a computer can calculate. For instance, the order of precedence rules, aka PEMDAS (parenthesis, exponents, multiplication, division, addition, subtraction) need to be factored into order of computation. To support these rules, in computer math we often convert a String expression into Reverse Polish Notation (RPN). This converts a simple string like "3 + 4" to become ["100", "200", "+"]) using the Shunting-yard algorithm.

String expression to calculation will need a flow of control. A Class (aka model) is defined with instance variables, one argument constructor, and a toString. In this code, the flow is in the Calculator calculation is contained in the constructor.

  • Capture original input
  • Parse expression into mathematical terms
  • If using Shunting-yard algorithm, place terms into RPN
  • Calculate RPN to a result
// Key instance variables
private final String expression;
private ArrayList<String> tokens;
private ArrayList<String> reverse_polish;
private Double result;


// Create a 1 argument constructor providing a mathematical expression
    public Calculator(String expression) {
        // original input
        this.expression = expression;

        // parse expression into terms
        this.termTokenizer();

        // place terms into reverse polish notation
        this.tokensToReversePolishNotation();

        // calculate reverse polish notation
        this.rpnToResult();
    }

    // Print the expression, terms, and result
    public String toString() {
        return ("Original expression: " + this.expression + "\n" +
                "Tokenized expression: " + this.tokens.toString() + "\n" +
                "Reverse Polish Notation: " +this.reverse_polish.toString() + "\n" +
                "Final result: " + String.format("%.2f", this.result));
    }

Term Tokenizer

FRQ3 talks about changing terms to tokens. A string containing text and possibly delimiters has been split into tokens and stored in String[] tokens. Each token is either an open delimiter, a close delimiter, or a substring that is not a delimiter. You will write the method getDelimitersList, which returns an ArrayList containing all the open and close delimiters found in tokens in their original order.

- openDel:"("    - closeDel: ")"
- tokens: "(" "100 + 200" ")" " * 3"
- ArrayList of delimiters: "(" ")"

The method termTokenizer() is used to change the String expression into a series of tokens that constitute distinct elements of a Mathematical expression. The result is retained in the ArrayList "tokens". Every token, including parenthesis, is an element in the "tokens" ArrayList.

- Example 1:Simple Math        - Original expression: 100 + 200  * 3
    - tokens ArrayList: [100, +, 200, *, 3]
- Example 2: Parenthesis Math to change order of operations
    - Original expression: (100 + 200)  * 3
    - tokens ArrayList: [(, 100, +, 200, ), *, 3]

String to Tokens

Before Tokenizer there is a String. During tokenization the Chars of the String are made into mathematical tokens.

  • Simple Math
    • String:"100+2003" - Tokens: "100", "+", "200", "", "3"
// Term Tokenizer takes original expression and converts it to ArrayList of tokens
private void termTokenizer() {
    // contains final list of tokens
    this.tokens = new ArrayList<>();

    int start = 0;  // term split starting index
    StringBuilder multiCharTerm = new StringBuilder();    // term holder
    for (int i = 0; i < this.expression.length(); i++) {
        Character c = this.expression.charAt(i);
        if ( isOperator(c.toString() ) || isSeperator(c.toString())  ) {
            // 1st check for working term and add if it exists
            if (multiCharTerm.length() > 0) {
                tokens.add(this.expression.substring(start, i));
            }
            // Add operator or parenthesis term to list
            if (c != ' ') {
                tokens.add(c.toString());
            }
            // Get ready for next term
            start = i + 1;
            multiCharTerm = new StringBuilder();
        } else {
            // multi character terms: numbers, functions, perhaps non-supported elements
            // Add next character to working term
            multiCharTerm.append(c);
        }

    }
    // Add last term
    if (multiCharTerm.length() > 0) {
        tokens.add(this.expression.substring(start));
    }
}

Tokens to RPN

Before calculation the tokens need to be turned into RPN. This puts math in left to right order and resolves order of operations.

  • Simple Math.
    • Tokens:"100", "+", "200", "", "3" - RPN: "100", "200", "3", "", "+"
  • Parenthesis Math. Note RPN difference
    • Tokens: "(", "100}, "+", "200", ")", "*", "3"
    • RPN: "100", "200", "+", "3", "*"
// Takes tokens and converts to Reverse Polish Notation (RPN).
private void tokensToReversePolishNotation () {
    // contains final list of tokens in RPN
    List<String> reverse_polish = new ArrayList<>();

    // stack is used to reorder for appropriate grouping and precedence
    Stack tokenStack = new Stack();
    for (String token : tokens) {
        switch (token) {
            // If left bracket push token on to stack
            case "(":
                tokenStack.push(token);
                break;
            case ")":
                while (tokenStack.peek() != null && !tokenStack.peek().equals("("))
                {
                    reverse_polish.add( (String)tokenStack.pop() );
                }
                tokenStack.pop();
                break;
            case "+":
            case "-":
            case "*":
            case "/":
            case "%":
                // While stack
                // not empty AND stack top element
                // and is an operator
                while (tokenStack.peek() != null && isOperator((String) tokenStack.peek()))
                {
                    if ( isPrecedent(token, (String) tokenStack.peek() )) {
                        reverse_polish.add((String)tokenStack.pop());
                        continue;
                    }
                    break;
                }
                // Push the new operator on the stack
                tokenStack.push(token);
                break;
            default:    // Default should be a number, there could be test here
                this.reverse_polish.add(token);
        }
    }
    // Empty remaining tokens
    while (tokenStack.peek() != null) {
        reverse_polish.add((String)tokenStack.pop());
    }

}

RPN to Result

Below is setup/pseudo code to produce a result. This is opportunity to learn about a Stack, by adding and removing elements from the stack according to intermediate and final results.

During calculation algorithm will work through RPN, Left to Right. 1. go to first operator, 2. obtain to the amount of operands required for operator, 3. solve, 4. push result

  • Simple Math.

    • RPN:"100", "200", "3", "*", "+" - calcStack Step 1: [100]
    • calcStack Step 2 [200, 100]
    • calcStack Step 3 [3, 200, 100]
    • calcStack Step 4 [600, 100]
    • calcStack Step 5 [700]

      result = 700 # final pop

// Takes RPN and produces a final result
private void rpnToResult()
{
    // stack is used to hold operands and each calculation
    Stack<Double> calcStack = new Stack<Double>();

    // RPN is processed, ultimately calcStack has final result
    for (String token : this.reverse_polish)
    {
        // If the token is an operator, calculate
        if (isOperator(token))
        {
            // Pop the two top entries

            // Calculate intermediate results
            result = 0.0;

            // Push intermediate result back onto the stack
            calcStack.push( result );
        }
        // else the token is a number push it onto the stack
        else
        {
            calcStack.push(Double.valueOf(token));
        }
    }
    // Pop final result and set as final result for expression
    this.result = calcStack.pop();
}

Driver / Tester

A Class should always have a driver for testing. In a driver for expressions, you will need to consider multiple conditions, for instance changes with precedence...

Output Examples

Here are sample outputs for the calculator

Simple Math
Original expression:100 + 200  * 3Tokenized expression: [100, +, 200, *, 3]
Reverse Polish Notation: [100, 200, 3, *, +]
Final result: 700.00

Parenthesis Math
Original expression: (100 + 200)  * 3
Tokenized expression: [(, 100, +, 200, ), *, 3]
Reverse Polish Notation: [100, 200, +, 3, *]
Final result: 900.00

Decimal Math
Original expression: 100.2 - 99.3
Tokenized expression: [100.2, -, 99.3]
Reverse Polish Notation: [100.2, 99.3, -]
Final result: 0.90

Modulo Math
Original expression: 300 % 200
Tokenized expression: [300, %, 200]
Reverse Polish Notation: [300, 200, %]
Final result: 100.00

Division Math
Original expression: 300/200
Tokenized expression: [300, /, 200]
Reverse Polish Notation: [300, 200, /]
Final result: 1.50

Hacks

Build a calculator to process expressions and ultimately change RPN to a calculation.

  1. Finish rpnToResult for Calculator
  2. Add unbalanced parenthesis check and in original FRQ, or other error checks. FYI, usually when structuring code with error checking it can greatly impact code structure.
  3. Build in Power of operator ^:2 ^ 1 = 2, 2 ^ 2 = 4, 2 ^ 3 = 83. Build an API to receive an expression and respond with a result. This is a good opportunity to respond with an error if you built in parenthesis or other error checking.

Advanced. Deeper parsing and evaluation.

  • Try adding single argument function SQRT. This should be combined with ()'s to make sense, SQRT(expression). Though "SQRT 1" could work.
  • Build variable into expression "a = 3; b + 4; SQRT(a^2 + b^2)". Hint... build a HashMap for variables.
  • At this point you probably have Physics and Calculus possibilities for managing and modeling equations.

Advanced. Deployment and Frontend.

  • Deploy all of your APIs to Team Backend repo.
  • Start making Frontend pages to interact with APIs.

Advanced. Try other evaluations or use cases of parsing Strings to Tokens.

  • Make a Tokenizer for sentences to words.
  • Count words, count punctuation.
  • Find definitions for Words or Translations.

Basic Skill building, do FRQ3. FYI, isBalanced could help in error corrections for Calculator.

public class Delimiters
private String openDel
private String closeDel
public Delimiters(String open, String close)
public ArrayList<String> getDelimitersList(String[] tokens)
public boolean isBalanced(ArrayList<String> delimiters)