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:

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