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