Search code examples
javaalgorithmnegative-numbermathematical-expressionsshunting-yard

Java - multiple levels of negative signs in a mathematical expression evaluator program


I've been trying to make a mathematical expression evaluator program (calculator) in java and I'm stuck with a problem: I can't find a way to make the program support mathematical expressions with multi-levels of negative signs.

The expression 1 - -(-(-(-4))) 's result should be -3.0, but I'm unable to make my program determine whether the number "4" is a negative number or not. My program returns 5 instead since it cannot determine correctly.

Another example is the expression 12*123/-(-5+2) It should return an answer of 492, but my program incorrectly returns -492.

Here's the code I've been working on:

import java.util.*;

public class MathEvaluator {

    // get the precedence order of an operator
    public static int getPrecedence(Character op) {
        switch (op) {
            case '*':
                return 2;
            case '/':
                return 2;
            case '+':
                return 1;
            case '-':
                return 1;
            default:
                return 0;
        }
    }

    // evaluating simple math expressions between two floating point numbers
    // order is reversed due to the stack sturcture (LIFO), b comes first while a
    // comes after
    public static double evalSimpleExpr(String operator, double a, double b) {
        switch (operator) {
            case "*":
                return b * a;
            case "/":
                if (a == 0)
                    return 0;
                return b / a;
            case "+":
                return b + a;
            case "-":
                return b - a;
            default:
                return 0;
        }
    }

    public static double calculate(String expression) {
        System.out.println("[DEBUG]: " + expression);
        // make a new array of characters while removing all the whitespaces
        char[] characters = expression.replaceAll("\\s+", "").toCharArray();

        List<String> output = new ArrayList<String>();
        Stack<Character> ops = new Stack<Character>();

        mainLoop: for (int i = 0; i < characters.length; i++) {

            // if element is an integer / floating-point number / decimal point, then
            // push it into the stack "numbers"
            if (Character.isDigit(characters[i]) || characters[i] == '.') {
                // if the character before the current element is still a digit/decimal point,
                // then ignore and continue scanning the next element
                if (i > 0 && (Character.isDigit(characters[i - 1]) || characters[i - 1] == '.'))
                    continue mainLoop;
                String digits = "";
                // get the negative sign (if it exists for the number) and add it to the
                // number
                if (i > 1 && characters[i - 1] == '-'
                        && (characters[i - 2] == '+' || characters[i - 2] == '-' || characters[i - 2] == '*'
                                || characters[i - 2] == '/' || characters[i - 2] == '(' || characters[i - 2] == ')')) {
                    digits += "-";
                } else if (i == 1 && characters[0] == '-') {
                    digits += "-";
                }
                // a loop to the right to get all digits in a number
                digits += characters[i];
                int j = i + 1;
                while (!(j >= characters.length) && (Character.isDigit(characters[j]) || characters[j] == '.')) {
                    digits += characters[j];
                    j += 1;
                }

                System.out.println("[NUMBER]: " + digits);
                output.add(digits);

                // check if element is brace
                // if its an opening brace, push it into the operator stack
            } else if (characters[i] == '(') {
                ops.push(characters[i]);

            // if its a closing brace, pop all the elements between the braces into the
            // output queue one by one
            } else if (characters[i] == ')') {
                while (!ops.empty() && ops.peek() != '(') {
                    output.add(Character.toString(ops.pop()));
                }
                ops.pop();

            // check if element is an operator
            } else if (characters[i] == '+' || characters[i] == '-' || characters[i] == '*' || characters[i] == '/') {
                // ignoring negative signs
                if (i > 0 && characters[i] == '-' && (characters[i - 1] == '+' ||
                        characters[i - 1] == '-'
                        || characters[i - 1] == '*' || characters[i - 1] == '/' || characters[i - 1] == '(')) {
                    continue mainLoop;
                } else if (i == 0 && characters[i] == '-') {
                    continue mainLoop;
                }
                /* 
                 * check if the previous operator has priority over the current operator or if
                 * the previous operator has the same priority as the current operator (if they
                 * have the same priority,
                 * then the previous operator should be applied before the current operator)
                 * and pop the elements from the stack until the current operator takes the
                 * priority
                */
                while (!ops.empty() && getPrecedence(ops.peek()) >= getPrecedence(characters[i])) {
                    output.add(Character.toString(ops.pop()));
                }
                ops.push(characters[i]);
            }
        }

        // push the rest of the operators from the stack to the output queue
        while (!ops.empty()) {
            output.add(Character.toString(ops.pop()));
        }

        System.out.println(output);

        return postfixEval(output);
    }

    // evaluating the postfix expression
    public static double postfixEval(List<String> postfix) {

        Stack<String> stack = new Stack<String>();

        for (String element : postfix) {
            // if the element is an operator and the stack has more than two elements, apply
            // the operator to the two last numbers from the stack
            if (stack.size() >= 2
                    && (element.equals("+") || element.equals("-") || element.equals("*") || element.equals("/"))) {
                stack.push(Double.toString(
                        evalSimpleExpr(element, Double.parseDouble(stack.pop()), Double.parseDouble(stack.pop()))));
            } else {
                stack.push(element);
            }
        }

        System.out.println(stack);
        return Double.parseDouble(stack.pop());
    }

    // test
    public static void main(String[] args) {
        calculate("15+2*-12-(0.5+0.8*(8-1))/-0.1+0.5+0.5");
        calculate("12* 123/-(-5 + 2)");
    }

}

Thanks!


Solution

  • A minus symbol can be just the first character of a signed number (which your code processes that way), or a binary subtraction operator (which your code deals with), but also a unary operator, which your code does not deal with. Instead (as the comment in your code says) such occurrence of a minus symbol is ignored by it.

    You would need to add logic to identify when a minus symbol represents a unary minus operator. Once you have that, you can also interpret a minus in front of an unsigned number as a unary minus operator so that the parsing of numbers can be limited to unsigned numbers only.

    As the postfixEval function will need to distinguish between a binary minus operator and a unary minus operator, you could introduce a different character for one of the two. For instance, you could stack a tilde (~) in case a unary minus is intended. In that case postfixEval should only pop one value from its stack and negate it.

    Some other remarks:

    • Instead of comparing a character with *, /, + and -, inside the calculate function, you could reuse the logic already implemented in getPrecedence. When that function returns a non-zero value, you know that the character was such an operator.
    • Instead of postfixEval converting calculated results back to string so they can be pushed back on the stack, have your stack store doubles, and convert in the other direction. This will involve fewer conversions.

    Here is the relevant part of your code updated with the above remarks. Comments indicate where changes occured:

    public class Main {
        public static int getPrecedence(Character op) {
            switch (op) {
                case '~': // Unary minus
                    return 101; // Greater than 100 means "unary"
                case '*':
                    return 2;
                case '/':
                    return 2;
                case '+':
                    return 1;
                case '-':
                    return 1;
                default:
                    return 0;
            }
        }
    
        public static double evalSimpleExpr(String operator, double a, double b) {
            switch (operator) {
                case "*":
                    return b * a;
                case "/":
                    if (a == 0)
                        return 0;
                    return b / a;
                case "+":
                    return b + a;
                case "-":
                    return b - a;
                case "~": // For unary minus (b is ignored)
                    return -a;
                default:
                    return 0;
            }
        }
    
        public static double calculate(String expression) {
            char[] characters = expression.replaceAll("\\s+", "").toCharArray();
            List<String> output = new ArrayList<String>();
            Stack<Character> ops = new Stack<Character>();
    
            for (int i = 0; i < characters.length; i++) {
                char token = characters[i]; // Let's use a variable for this
                // If element represents an unsigned number, then push it into the stack "output"
                if (Character.isDigit(token) || token == '.') {
                    // Don't deal with negative sign -- deal with it elsewhere as a unary operator
                    String digits = "";
                    // Collect all digits (and optional decimal point)
                    for (; i < characters.length && (Character.isDigit(characters[i]) || characters[i] == '.'); i++) {
                        digits += characters[i];
                    }
                    i--;
                    output.add(digits);
                } else if (token == '(') {
                    ops.push(token);
                } else if (token == ')') {
                    while (!ops.empty() && ops.peek() != '(') {
                        output.add(Character.toString(ops.pop()));
                    }
                    ops.pop();
                } else if (getPrecedence(token) > 0) {
                    // Determine whether operator is unary
                    if (i == 0 || getPrecedence(characters[i - 1]) > 0 || characters[i - 1] == '(') {
                        if (token != '-') continue; // Ignore unary + (it has no effect)
                        ops.push('~'); // Use a distinctive character for unary minus.
                    } else {
                        while (!ops.empty() && getPrecedence(ops.peek()) >= getPrecedence(token)) {
                            output.add(Character.toString(ops.pop()));
                        }
                        ops.push(token);
                    }
                }
            }
    
            while (!ops.empty()) {
                output.add(Character.toString(ops.pop()));
            }
            return postfixEval(output);
        }
    
        public static double postfixEval(List<String> postfix) {
            Stack<Double> stack = new Stack<Double>(); // Use Double instead of String
    
            for (String element : postfix) {
                int prec = getPrecedence(element.charAt(0));
                if (prec >= 100) { // Unary 
                    stack.push(evalSimpleExpr(element, stack.pop(), 0));
                } else if (prec > 0) { // Binary
                    stack.push(evalSimpleExpr(element, stack.pop(), stack.pop()));
                } else {
                    stack.push(Double.parseDouble(element));
                }
            }
    
            return stack.pop();
        }