import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.Scanner;
/* In mathematics,
    an expression or mathematical expression is a finite combination of symbols that is well-formed
    according to rules that depend on the context.
   In computers,
    expression can be hard to calculate with precedence rules and user input errors
    to handle computer math we often convert strings into reverse polish notation
    to handle errors we perform try / catch or set default conditions to trap errors
     */
public class Calculator {
    // Key instance variables
    private final String expression;
    private ArrayList<String> tokens;
    private ArrayList<String> reverse_polish;
    private Double result = 0.0;
    public boolean test_continue;

    // Helper definition for supported operators
    private final Map<String, Integer> OPERATORS = new HashMap<>();
    {
        // Map<"token", precedence>
        OPERATORS.put("^", 2);
        OPERATORS.put("R", 2);
        OPERATORS.put("*", 3);
        OPERATORS.put("/", 3);
        OPERATORS.put("%", 3);
        OPERATORS.put("+", 4);
        OPERATORS.put("-", 4);
    }

    // Helper definition for supported operators
    private final Map<String, Integer> SEPARATORS = new HashMap<>();
    {
        // Map<"separator", not_used>
        SEPARATORS.put(" ", 0);
        SEPARATORS.put("(", 0);
        SEPARATORS.put(")", 0);
    }

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

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

     
        // place terms into reverse polish notation
        this.tokensToReversePolishNotation();
        // calculate reverse polish notation
        if (this.tokensToReversePolishNotation()){
        this.rpnToResult();
        }
        else{
            System.out.println("error with paranthesis");
        }
    }

    // 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) ;
    }

    // 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() ) || isSeparator(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));
        }
        System.out.println(tokens);
    }

    public boolean valid_paranthesis(){
        int open_para_counter = 0;
        int close_para_counter = 0;
        ArrayList<String> tokens_strings = tokens;

        for (int i =0; i < tokens_strings.size(); i++){
            String variable = tokens_strings.get(i);
            
            if (variable.equals("(") || variable.equals(" (")){
                open_para_counter = open_para_counter +1;
                
            }

            if (variable.equals(")") || variable.equals(" )")){
                close_para_counter = close_para_counter +1;
                
            }
        }
        if (open_para_counter == close_para_counter){
            return true;
        }

        return false;
    }
    

    // Takes tokens and converts to Reverse Polish Notation (RPN), this is one where the operator follows its operands.

    private boolean tokensToReversePolishNotation () {
        // contains final list of tokens in RPN
        this.reverse_polish = new ArrayList<>();
        if (valid_paranthesis()){

            // stack is used to reorder for appropriate grouping and precedence
            Stack<String> tokenStack = new Stack<String>();
            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( tokenStack.pop() );
                        }
                        tokenStack.pop();
                        break;
                    case "+":
                    case "-":
                    case "*":
                    case "/":
                    case "%":
                    case "^":
                    case "R":
                        // While stack
                        // not empty AND stack top element
                        // and is an operator
                        while (tokenStack.size() > 0 && isOperator(tokenStack.peek()))
                        {
                            if ( isPrecedent(token, tokenStack.peek() )) {
                                reverse_polish.add(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.size() > 0) {
                reverse_polish.add(tokenStack.pop());
            }

            test_continue = true;
            return test_continue;
        }
        else{
            return test_continue;
        }

    }

    // 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
                Double entry1 = calcStack.pop();
                Double entry2 = calcStack.pop();
                
                // Calculate intermediate results
                if(token.equals("+"))
                {
                    result = entry1 + entry2;
                }
                else if(token.equals("-")){
                    result = entry1 - entry2;
                }
                else if(token.equals("*")){
                    result = entry1 * entry2;
                }
                else if(token.equals("/")){
                    result = entry2 / entry1;
                }
                else if(token.equals("%")){
                    result = entry2 % entry1;
                }
                else if(token.equals("^")){
                    result = Math.pow(entry2, entry1);
                }
                else if(token.equals("R")){
                    result = Math.pow(entry2, (1/ entry1));
                }
             

                // 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();
    }

    // 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));
    }


    public String jsonify() {
        String json = "{ \"Expression\": \"" + this.expression + "\", \"Tokenized Expression\": \"" + this.tokens + 
        "\", \"Reverse Polish Notation\": \"" + this.reverse_polish + "\", \"Result\": " + this.result + " }";
        return json;
    }

    // Tester method
    public static void main(String[] args) {
        // Random set of test cases
         // Random set of test cases
         Calculator simpleMath = new Calculator("100 + 200  * 3");
         System.out.println("Simple Math\n" + simpleMath);
 
         System.out.println();
 
         Calculator parenthesisMath = new Calculator("(100 + 200)  * 3");
         System.out.println("Parenthesis Math\n" + parenthesisMath);
 
         System.out.println();
 
         Calculator decimalMath = new Calculator("100.2 - 99.3");
         System.out.println("Decimal Math\n" + decimalMath);
 
         System.out.println();
 
         Calculator moduloMath = new Calculator("300 % 200");
         System.out.println("Modulo Math\n" + moduloMath);
 
         System.out.println();
 
         Calculator divisionMath = new Calculator("300/200");
         System.out.println("Division Math\n" + divisionMath);

         System.out.println();
         Calculator unfinished_paranthesis = new Calculator("(27/3");
         System.out.println("Division Math\n" + unfinished_paranthesis);

         System.out.println();
         Calculator exponent = new Calculator("2^8");
         System.out.println("Exponent Math\n" + exponent);

         System.out.println();

         Calculator sqrt = new Calculator("125R3");
         System.out.println("Exponent Math\n" + sqrt);

         System.out.println();
         Scanner myObj = new Scanner(System.in);  // Create a Scanner object     
         String expression_user = myObj.nextLine();  // Read user input
         Calculator input = new Calculator(expression_user);
         System.out.println("Exponent Math\n" + input);
        
         System.out.println();
 

    }
}

Calculator.main(null);
100 + 200  * 3
[100, +, 200, *, 3]
Simple Math
Original expression: 100 + 200  * 3
Tokenized expression: [100, +, 200, *, 3]
Reverse Polish Notation: [100, 200, 3, *, +]
Final result: 700.00

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

100.2 - 99.3
[100.2, -, 99.3]
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

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

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

(27/3
[(, 27, /, 3]
error with paranthesis
Division Math
Original expression: (27/3
Tokenized expression: [(, 27, /, 3]
Reverse Polish Notation: []
Final result: 0.00

2^8
[2, ^, 8]
Exponent Math
Original expression: 2^8
Tokenized expression: [2, ^, 8]
Reverse Polish Notation: [2, 8, ^]
Final result: 256.00

125R3
[125, R, 3]
Exponent Math
Original expression: 125R3
Tokenized expression: [125, R, 3]
Reverse Polish Notation: [125, 3, R]
Final result: 5.00

300+200
[300, +, 200]
Exponent Math
Original expression: 300+200
Tokenized expression: [300, +, 200]
Reverse Polish Notation: [300, 200, +]
Final result: 500.00

Extras

  • Implemented a square root function
  • Implemented an API controller frontend
  • User input for the calculator through scanner object