import re
import math
import tkinter as tk
class Calculator:
def __init__(self): #FUNCTION 1 INNIT
#PEMDAS DICTIONARY TO HANDLE ORDER OF PRECEDENCE VIA KEY-VALUES
self.PEMDAS = {'+': 4, '-': 4, '*': 5, '/': 5, '%': 9, '√': 7, "^": 10, '(': 1, ')': 1, "[": 2, "]": 2, "{": 3, "}": 3 }
self.root = tk.Tk()# creates the main application window using the tkinter library
self.root.title("Calculator") #This sets the title of the root window to "Calculator
self.expression_entry = tk.Entry(self.root, justify='right', font=('Arial', 22)) #widget is where the user can input their mathematical expressions
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'),
]
#starts a for loop that iterates through each tuple in the buttons with their specifications
# a lambda function is used to Specify the function to be executed when the button is clicked.
# It uses a lambda function to pass the text value (button label) as an argument to the self.button_click method.
# This allows the button_click method to know which button was clicked.
for (text, row, col, bg_color, fg_color) in buttons:
button = tk.Button(self.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)
self.root.mainloop()
def button_click(self, char): #FUNCTION 2
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 self.is_valid_expression(expression): #It checks if the expression is valid using the is_valid_expression method.
tokens = self.unary_negation_EVALUATION(expression, self.unary_negation_TRUE_FALSE(expression)) #Tokenizes the expression
postfix = self.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, "Invalid Input")
def is_valid_expression(self, expression): #Checks if expression is valid using the Regex library(regular expression)
expression = expression.replace(" ", "")
if re.match(r'^[-^+*/()%.√\d\s\[\]{}]+$', expression) or re.match(r'^\d+%\s*$', expression):
return expression
def unary_negation_EVALUATION(self, expression, unary_negation_exists):
expression = expression.replace(" ", "") # Remove spaces from the input expression.
if unary_negation_exists:
special_components = [] # Initialize a list to store components of the expression.
# Define a regular expression pattern to tokenize the expression.
pattern = r'(-?\d+\.\d*|-?\d*\.\d*|-?\d+|-|\D)|[-+\*/\(\)\^]+'
# Use the regular expression pattern to find all tokens in the expression.
tokens = re.findall(pattern, expression)
# Iterate through the tokens.
for token in tokens:
if token == '-' and (not special_components or not special_components[-1].isdigit()):
# Check if the current token is a hyphen and if either:
# - The list of special components is empty.
# - OR the last element in the list is not a digit.
special_components.append('-1') # Append '-1' to the special components list.
else:
special_components.append(token) # Append the current token to the special components list.
# Return the result of applying unary negation handling to the special components.
return self.negative_1_times_following_N(special_components)
else:
# If unary negation does not exist, tokenize the expression using a different method.
return self.tokenize(expression)
def unary_negation_TRUE_FALSE(self, expression):
unary_negation_exists = False # Initialize a flag to track the existence of unary negation.
# Iterate through each character in the input expression.
for char in range(len(expression)):
if expression[char] == '-' and (char == 0 or not expression[char - 1].isdigit()):
# Check if the current character is a hyphen (-) and the following conditions are met:
# - It's the first character in the expression (char == 0).
# - OR it's not preceded by a digit (not expression[char - 1].isdigit()).
unary_negation_exists = True # Set the flag to True, indicating that unary negation exists.
break # Exit the loop since we found a unary negation.
if unary_negation_exists:
return True # Return True if unary negation exists in the expression.
else:
return False # Return False if unary negation does not exist in the expression.
def tokenize(self, expression): # Function to tokenize
if "." not in expression: #if there are no decimals the code uses this regular expression, that accepts only non-decimal and
#non numbers
valid_components = re.findall(r'(\d+|\+|-|\*|/|\(|\)|\[|\]|\{|\^|√|%)', expression)
return valid_components
elif "." in expression and " " in expression:
valid_components = re.findall(r'(\d+\.\d+|\d+|%|\D)|[-+\*/\(\)\^]+', expression)
return valid_components
elif "." in expression and " " not in expression: #Regular expression that stores decimals
valid_components = re.findall(r'(\d+\.\d+|\d+|\.\d+|%|\D)|[-+\*/\(\)\^]+', expression)
return valid_components
def negative_1_times_following_N(self, tokens): #function in charge of converting tokens to -1
i = 0 # Initialize an index variable i to 0 to iterate through the list of tokens.
while i < len(tokens):
# Check if the current token is "-1"
if tokens[i] == "-1":
# If the current token is "-1," it indicates a unary negation.
# Check if there is a next token (i.e., if i + 1 is within the bounds of the list) and if it is a digit.
if i + 1 < len(tokens) and tokens[i + 1].isdigit():
# If the next token is a digit, it means we have a negative number to handle.
# Convert the next token (the digit) to an integer, negate it, and convert it back to a string.
tokens[i] = str(-int(tokens[i + 1]))
# Delete the next token (the digit) since it's no longer needed.
del tokens[i + 1]
else:
# If the next token is not a digit, it means "-1" is not a unary negation but a subtraction.
# In this case, delete the current token ("-1").
del tokens[i]
else:
# If the current token is not "-1," move to the next token by incrementing the index variable i.
i += 1
# After processing all tokens, return the modified list of tokens.
return tokens
def shunting_yard(self, tokens):
operators = [] # Initialize an empty stack for operators.
output = [] # Initialize an empty list for the output in postfix notation.
for token in tokens:
if token in '+-*/^√%':
# If the token is an operator (+, -, *, /, ^, √, or %), handle operator precedence.
# Check if the operators stack is not empty and if the precedence of the current token
# is less than or equal to the precedence of the operator at the top of the stack.
while operators and operators[-1] in '+-*/√%' and self.PEMDAS[token] <= self.PEMDAS[operators[-1]]:
# Pop operators from the stack and append them to the output until the conditions are met.
output.append(operators.pop())
# Push the current token onto the operators stack.
operators.append(token)
elif token in '([{':
# If the token is an opening parenthesis, bracket, or brace (e.g., (, [, {), push it onto the operators stack.
operators.append(token)
elif token in ')]}':
# If the token is a closing parenthesis, bracket, or brace (e.g., ), ], }), handle matching pairs.
# Pop operators from the stack and append them to the output until an opening parenthesis is encountered.
while operators and operators[-1] not in '([{':
output.append(operators.pop())
# Check if there is a matching opening parenthesis at the top of the stack.
if operators and operators[-1] in '([{':
operators.pop() # Pop and discard the matching opening parenthesis.
else:
raise ValueError("Mismatched parentheses, brackets, or braces")
else:
# If the token is not an operator or parenthesis, it's an operand (number or other).
# Append it directly to the output.
output.append(token)
# After processing all tokens, any remaining operators on the stack should be popped and appended to the output.
while operators:
output.append(operators.pop())
return output # Return the list of tokens in postfix (RPN) notation.
def evaluate_postfix(self, tokens):
stack = []
for token in tokens: #Iterates through each character, stored into the stack and if the character is an operator
#performs the following logical mathematical depending on which operator is encountered
if token in '+-*/^√%':
if token == '-':
if stack and stack[-1] == '%': #special case for subtraction by a percentage
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] == '%':#special case for addition by a percentage
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] == '%':#special case for multiplication by a percentage
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] == '%': #special case for division by a percentage
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] == '%': #special case for exponent by a percentage
stack.pop()
b = 0.01
else:
b = 1.0
b_exponent = float(stack.pop())
a_base = float(stack.pop())
result = a_base ** b_exponent * b
stack.append(str(result))
elif token == '√':
if stack and stack[-1] == '%':
stack.pop()
b = 0.01
else:
b = 1.0
a = float(stack.pop())
result = math.sqrt(a) * b
stack.append(str(result))
elif token == '%':
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
group_tokens = self.tokenize(group_contents) # Tokenize the contents
group_postfix = self.shunting_yard(group_tokens) # Convert to postfix notation
group_result = self.evaluate_postfix(group_postfix) # Evaluate the contents
stack.append(str(group_result)) # Replace the group contents with the result
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
group_tokens = self.tokenize(group_contents) # Tokenize the contents
group_postfix = self.shunting_yard(group_tokens) # Convert to postfix notation
group_result = self.evaluate_postfix(group_postfix) # Evaluate the contents
stack.append(str(group_result)) # Replace the group contents with the result
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)))
elif token != '%': # Skip '%' when performing multiplication
stack.append(token)
# Calculate the result by multiplying numeric values left in the stack
numeric_values = [float(value) if value != '%' else 1.0 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(): #runs main function
calculator = Calculator()
calculator.run()
if __name__ == '__main__':
main()
这是我的PEMDAS计算器。我相信它几乎完成了,但是,当涉及到解决一个小细节时,我仍然面临困难。当用户输入-5-5或两个或更多负数时,它会将它们彼此相乘。因此,-5-5输入给出了25的错误答案。我相信这是因为我的代码错误地将-5-5存储为令牌列表,如下所示:
'-5','-5']
这是不正确的,因为我的代码实际上会将两个字符相乘,我需要的是代码将标记存储为
'-5','5','-'],以便它根据evaluate_postfix函数的逻辑执行减法。
有没有办法解决这个问题?
我尝试在一个替代代码中使用正则表达式来检测'-5'作为一个数字,而不是isdigit函数,但我还没有得到任何成功的结果。
1条答案
按热度按时间tf7tbtn21#
我添加了一个调试行
所以我们可以看看会发生什么。以下是三种表达方式及其解释
在前4个中,标记器正确地解析了事物,但在第5个中,它错误地标记了表达式。
“-”被解释为一元“-”还是二元“-"取决于上下文。在一个字符串的开头,或者在一个“*”之后,它将是一个一元“-”,但是如果它跟在一个数字之后,它将是一个二进制“-”。
因此,您不能在标记化步骤中解决问题,这应该是上下文无关的。最好保持标记化的简单性,将复杂的逻辑留给调度场步骤。
我总是把“-”当作一个单独的标记。因此,'-5'标记为'-','5']。这很容易实现,只需使用
然后添加一个方法来识别和使用前缀表达式的标记。
调车场的算法现在需要修改,所以它首先读取一个前缀表达式,然后是一个二元运算符,然后是一个前缀表达式。
有了上面的代码和一个额外的调试行,我们就有了
您需要修改代码,以便更好地测试数字或其他终端。