pythoncalculatorpostfix-notationshunting-yard

Unary negation ignored in user input in my pemdas calculator using python


I'm having issues with my PEMDAS calculator built in python. The issue that I'm encountering is mainly that I can't find a way to make my code recognize the "-" sign at the beginning of the user's input. So when a user submits an input like -5+5 , the code will completely dismiss the block in charge of handling cases in which there exist unary negation and simply perform a normal addition.

Which ends up in an incorrect output of -10, instead of Adding -5 to positive 5, which should give 0 as output

Here is my code:

import re
import math
import tkinter as tk

PEMDAS = {'+': 4, '-': 4, '*': 5, '/': 5, '%': 9, '√': 7, "^": 8, '(': 1, ')': 1, "[": 2, "]": 2, "{": 3, "}": 3}


def is_valid_expression(expression):
    return re.match(r'^[-^+*/()%.√\d\s\[\]{}]+$', expression) or re.match(r'^\d+%\s*$', expression)

def tokenize(expression):
    separated_expression = ""

    if expression.startswith("-"):
        valid_components = ["-1"] + re.findall(r'(\d+\.\d+|\d+|\.\d+|%|\D)|[-+\*/\(\)\^]+', expression[1:])
        return valid_components
    elif "." not in expression:
        separated_expression = expression.replace(" ", "")
        valid_components = re.findall(r'(\d+|\+|-|\*|/|\(|\)|\[|\]|\{|\^|√|%)', separated_expression)
        return valid_components
    elif "." in expression and " " in expression:
        separated_expression = expression.replace(" ", "")
        valid_components = re.findall(r'(\d+\.\d+|\d+|%|\D)|[-+\*/\(\)\^]+', separated_expression)
        return valid_components
    elif "." in expression and " " not in expression:
        valid_components = re.findall(r'(\d+\.\d+|\d+|\.\d+|%|\D)|[-+\*/\(\)\^]+', expression)
        return valid_components


def shunting_yard(tokens):
    operators = []  # stack that will hold operators as they are processed
    output = []  # list that will hold the postfix expression.

    # iterates through each token
    for token in tokens:
        # if the token is an operator
        if token in '+-*/^√%':
            # it checks the precedence of each operator using the PEMDAS dictionary
            # and compares it to the precedence of operators in the operators stack.
            while operators and operators[-1] in '+-*/√%' and PEMDAS[token] <= PEMDAS[operators[-1]]:
                output.append(operators.pop())  # It pops operators from the stack and appends them to
                # the output list until the conditions for precedence are satisfied,

            operators.append(token)  # then it pushes the current token onto the operators stack.
        elif token in '([{':
            # If the token is an opening parenthesis, square bracket, or curly brace
            operators.append(token)  # it's pushed into the operators stack.
        elif token in ')]}':
            # If the token is a closing parenthesis, square bracket, or curly brace,
            # it pops operators from the operators stack and appends them to the output
            # list until a matching opening parenthesis, square bracket, or curly brace is encountered.
            while operators and operators[-1] not in '([{':
                output.append(operators.pop())
            if operators and operators[-1] in '([{':
                operators.pop()  # Remove the matching opening parenthesis, square bracket, or curly brace
            else:
                raise ValueError("Mismatched parentheses, brackets, or braces")
        else:
            # if it's not an operator or parenthesis, it's an operand, so it's appended to the output list.
            output.append(token)

    # After processing all tokens,
    # any remaining operators in the operators stack are popped and appended to
    # the output list to complete the conversion.
    while operators:
        output.append(operators.pop())
    return output



class Calculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Calculator")
        #priority from each operator including parenthesis and brackets and % operator
        # Create an entry widget to display and input expressions
        self.expression_entry = tk.Entry(root, justify='right', font=('Arial', 22))
        self.expression_entry.grid(row=0, column=0, columnspan=4, padx=10, pady=10)


        #list of tuples named buttons is defined. 
        # Each tuple contains the text to be displayed on the button, 
        # and its row and column position in the grid layout. #setting up the color as well
        buttons = [
            ('7', 1, 0, '#CCCCCC', 'black'), ('8', 1, 1, '#CCCCCC', 'black'), ('9', 1, 2, '#CCCCCC', 'black'), ('/', 1, 3, '#CCCCCC', 'black'),
            ('4', 2, 0, '#CCCCCC', 'black'), ('5', 2, 1, '#CCCCCC', 'black'), ('6', 2, 2, '#CCCCCC', 'black'), ('*', 2, 3, '#CCCCCC', 'black'),
            ('1', 3, 0, '#CCCCCC', 'black'), ('2', 3, 1, '#CCCCCC', 'black'), ('3', 3, 2, '#CCCCCC', 'black'), ('-', 3, 3, '#CCCCCC', 'black'),
            ('0', 4, 0, '#CCCCCC', 'black'), ('.', 4, 1, '#CCCCCC', 'black'), (']', 4, 2, '#CCCCCC', 'black'), ('+', 4, 3, '#CCCCCC', 'black'),
            ('(', 5, 0, '#CCCCCC', 'black'), (')', 5, 1, '#CCCCCC', 'black'), ('[', 5, 2, '#CCCCCC', 'black'), ('%', 5, 3, '#CCCCCC', 'black'),
            ('{', 6, 0, '#CCCCCC', 'black'), ('}', 6, 1, '#CCCCCC', 'black'), ('√', 6, 2, '#CCCCCC', 'black'), ('^', 6, 3, '#CCCCCC', 'black'),
            ('C', 7, 1, '#CCCCCC', 'red'), ('←', 7, 2, '#CCCCCC', 'red'), ("=", 7, 3, '#CCCCCC', 'black'),
        ]

        for (text, row, col, bg_color, fg_color) in buttons:
            button = tk.Button(root, text=text, font=('Arial', 16), width=5, height=2, bg=bg_color, fg=fg_color, command=lambda t=text: self.button_click(t))
            button.grid(row=row, column=col, padx=5, pady=5)       

    def button_click(self, char):
        # try:
            if char == '=': #If the clicked button is '=', it signifies the user wants to evaluate the expression:
                expression = self.expression_entry.get() #It gets the expression from the expression_entry widget.

                #It checks if the expression ends with a number followed by '%' (e.g., "50%"). 
                # If so, it removes the '%' and adds division by 100 to handle percentages.
                if re.match(r'^\d+%\s*$', expression): #
                    expression = expression[:-1]  # Remove the %
                    expression = f"{expression} / 100"  # Add division by 100


                if is_valid_expression(expression): #It checks if the expression is valid using the is_valid_expression method.
                    tokens = tokenize(expression) #Tokenizes the expression
                    postfix = shunting_yard(tokens) #converts tokenized expression into postfix notation
                    evaluation = self.evaluate_postfix(postfix) #evaluates postfix converted expression
                    # updates the result in the expression_entry widget.
                    self.expression_entry.delete(0, tk.END)
                    self.expression_entry.insert(0, str(evaluation))
                else:
                    #If invalid, it shows "Invalid Input".
                    self.expression_entry.delete(0, tk.END)
                    self.expression_entry.insert(0, "Invalid Input")
        #If the clicked button is 'C', it clears the contents of the expression_entry widget.        
            elif char == 'C':
                self.expression_entry.delete(0, tk.END)
            #If the clicked button is '←', 
            #it removes the last character from the text in the expression_entry widget.
            elif char == '←':
                current_text = self.expression_entry.get()
                self.expression_entry.delete(0, tk.END)
                self.expression_entry.insert(0, current_text[:-1])
            #appends symbols depending on EACH OPERATOR INTRODUCED BY THE USER.
            elif char == '^':
                self.expression_entry.insert(tk.END, '^')
            elif char == '√':
                self.expression_entry.insert(tk.END, '√')
            elif char in '+-':
                if self.expression_entry.get():
                    last_char = self.expression_entry.get()[-1]
                    if last_char in '*/':
                        self.expression_entry.delete(len(self.expression_entry.get()) - 1, tk.END)
                        self.expression_entry.insert(tk.END, f' {last_char} {char} ')
                    elif last_char == '-':
                        # Check if the previous character is also a '-'
                        # If yes, treat the current '-' as a unary negation
                        self.expression_entry.delete(len(self.expression_entry.get()) - 1, tk.END)
                        self.expression_entry.insert(tk.END, ' -1 ')
                    else:
                        self.expression_entry.insert(tk.END, char)
                else:  # Handle unary negation at the start of expression
                    self.expression_entry.insert(tk.END, char)  # This is the part to add
            elif char in '*/%':
                if self.expression_entry.get():
                    last_char = self.expression_entry.get()[-1]
                    if last_char in '+-':
                        self.expression_entry.delete(len(self.expression_entry.get()) - 1, tk.END)
                        self.expression_entry.insert(tk.END, f' {last_char} {char} ')
                    else:
                        self.expression_entry.insert(tk.END, char)
            else:
                self.expression_entry.insert(tk.END, char)
        # except(IndexError, ValueError):
        #     self.expression_entry.delete(0, tk.END)
        #     self.expression_entry.insert(0, "Syntax Error")

    def evaluate_postfix(self, tokens):
        stack = []

        for token in tokens:
            if token in '+-*/^√%':
                if token == '-':
                    if not stack or stack[-1] in '({[':
                        stack.append("-1")  # Push the -1 token as a marker for unary 
                    else:
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a - b))
                elif token == '+':
                    if stack and stack[-1] == '-1' or stack and stack[-2] == '-2':
                        stack.pop()
                        operand = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(-operand))
                    elif stack and stack[-1] == '%':
                        stack.pop()
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a + (a * b * 0.01)))
                    else:
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a + b))
                elif token == '*':
                    if stack and stack[-1] == '%':
                        stack.pop()
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a * (b * 0.01)))
                    else:
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a * b))
                elif token == '/':
                    if stack and stack[-1] == '%':
                        stack.pop()
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a / (b * 0.01)))
                    else:
                        b = float(stack.pop())
                        a = float(stack.pop())
                        stack.append(str(a / b))
                elif token == '^':
                    if stack and stack[-1] == '%':
                        stack.pop()  # Remove the %
                        b = 0.01  # Convert percentage to fraction
                    else:
                        b = 1.0  # No percentage, use 1.0 as multiplier

                    b_exponent = float(stack.pop())  # The exponent
                    a_base = float(stack.pop())  # The base
                    result = a_base ** b_exponent * b
                    stack.append(str(result))

                elif token == '√':
                    if stack and stack[-1] == '%':
                        stack.pop()  # Remove the %
                        b = 0.01  # Convert percentage to fraction
                    else:
                        b = 1.0  # No percentage, use 1.0 as multiplier

                    a = float(stack.pop())
                    result = math.sqrt(a) * b
                    stack.append(str(result))

                elif token == '%':
                    stack.append(token)
            elif token == '-1':
                if stack:
                    operand = float(stack.pop())
                    stack.append(str(-operand))  # Apply unary negation to the operand
                else:
                    stack.append(token)
            else:
                stack.append(token)

        result = self.evaluate_grouped_parentheses(stack)
        return result




    def evaluate_grouped_parentheses(self, tokens):
        stack = []
        current_group_value = 1.0
        current_group_operator = None
        unary_negation = False  # Flag to track unary negation

        for token in tokens:
            if token in '({[':
                # Start of a new parentheses group
                if current_group_operator:
                    raise ValueError("Consecutive group operators are not allowed")
                current_group_operator = token
                stack.append(token)
            elif token in ')}]':
                # End of a parentheses group, evaluate the group
                if not current_group_operator:
                    raise ValueError("Unmatched closing group operator")
                if current_group_operator == '{' and token == '}':
                    # Evaluate the contents of the curly braces group
                    group_contents = stack.pop()  # Get the contents of the curly braces 
                    group_tokens = tokenize(group_contents)  # Tokenize the contents
                    group_postfix = shunting_yard(group_tokens)  # Convert to postfix 
                    group_result = self.evaluate_postfix(group_postfix)  # Evaluate the 
                    stack.append(str(group_result))  # Replace the group contents with the 
                elif current_group_operator == '[' and token == ']':
                    # Evaluate the contents of the square brackets group
                    group_contents = stack.pop()  # Get the contents of the square brackets 
                    group_tokens = tokenize(group_contents)  # Tokenize the contents
                    group_postfix = shunting_yard(group_tokens)  # Convert to postfix 
                    group_result = self.evaluate_postfix(group_postfix)  # Evaluate the 
                    stack.append(str(group_result))  # Replace the group contents with the 
                else:
                    raise ValueError("Mismatched group operators")
                current_group_operator = None
            else:
                if current_group_operator:
                    stack.append(token)
                else:
                    if unary_negation:
                        unary_negation = False
                        stack.append(str(-float(token)))
                    else:
                        stack.append(token)

        # Calculate the result by multiplying numeric values left in the stack
        numeric_values = [float(value) for value in stack if value not in '({[}])']
        result = 1.0
        for value in numeric_values:
            result *= value

        return result * current_group_value





def main():
        root = tk.Tk()
        calculator = Calculator(root)
        root.mainloop()
if __name__ == '__main__':
    main()

Solution

  • You need to add code to treat unary operators separately to binary operators. '-' is especially tricky as it can either be a unary or binary operator, working out which is context dependant.

    Let P represents prefix sequence, either a simple number like 5, or prefix expression like -5 or √9, or bracketed expressions (3+4) or even cases with multiple prefix operators -√9.

    Your expression then has the form P or P op P or P op P op P etc., where op is a binary operator. This allows for expressions like 4 * -5.

    The key part here is the first thing you will encounter is a prefix sequence. So the main loop needs to be like

    def E:
      while more_tokens
        P() // read a prefix sequence
    
        if next_token in '+-*/%': // just binary ops
           process_binary_operator
        else if token is EOF or closing bracket
           return
        else
           error
    
    def P:
        while next_token in '-√': // prefix ops
           process_unary_op
        if next_token in '([{':
           E() // call main E function recursively 
           check next_token is matching closing brace
        else if next_token is digit
           output.append(token)
    

    I've followed the shunting yard algorithm by Theodore Norvell at https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm