Files
pmpv-python/pmpv.py
2024-01-25 15:50:31 -05:00

235 lines
7.7 KiB
Python

"""
pmpv.py
Description: A simple command line calculator that supports addition, subtraction, variables, and parentheses
Author: Robert (Nayan) Sawyer
Date: 2024-01-24
Licence: MIT
Dependencies: Python's built-in re module, and two local modules: Variables.py and eprint.py
Comments: This program was written for a homework assignment for COS 301: Programming Languages at the University of Maine
"""
import re, sys
class Variables:
''' A singleton class that stores all variables in the program.
This is a singleton so that we can access the variables from anywhere in the program.'''
__variables = {}
__instance = None
def __init__(self):
if Variables.__instance != None:
raise Exception("This class is a singleton!")
else:
Variables.__instance = self
@classmethod
def get_instance(self):
if Variables.__instance == None:
Variables()
return Variables.__instance
def get(self, name):
if name not in self.__variables:
return None
return self.__variables[name]
def get_all(self):
yield from self.__variables
def contains(self, name):
return name in self.__variables
def set(self, name, value):
self.__variables[name] = value
def clear(self):
self.__variables = {}
def __str__(self):
return str(self.__variables)
def __repr__(self):
return str(self.__variables)
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
pmpv = {
'number': r'(?:(?:^-)*|(?:(?<=[ \()])-))?[0-9]+',
'identifier': r'[a-zA-Z]+',
'plus': r'\+',
'minus': r'\-',
'left_paren': r'\(',
'right_paren': r'\)',
'equals': r'\=',
}
compound_pmpv = '|'.join(pmpv.values())
def tokenize(userInput):
''' Tokenize the user input into a list of tokens '''
equals = userInput.count("=")
# Check for parentheses mismatch
if userInput.count("(") != userInput.count(")"):
eprint("Invalid expression: mismatched parentheses")
return None
# Check for invalid assignment characters
if userInput.count("=") > 1:
eprint("Invalid expression: too many '='")
return None
# Use regex to tokenize the input
tokens = re.findall(compound_pmpv, userInput)
# Check that the tokens are valid
for i,token in enumerate(tokens):
# Check for valid assignment syntax
if equals == 1 and i == 0:
if not re.match(pmpv['identifier'], token) and \
not tokens[i+1] == "=":
eprint("Invalid expression: expected '='")
return None
continue
# Replace variables with their values
if re.match(pmpv['identifier'], token):
var = Variables.get_instance().get(token)
if var == None:
eprint("Invalid expression: variable not defined")
return None
else:
tokens[i] = var
# Convert numbers to integers
if re.match(pmpv['number'], token):
tokens[i] = int(token)
return tokens
def evaluate_tokens(tokens):
''' Evaluate the tokens into a single value '''
left = None
right = None
operator = None
i = 0
def paren_recurse():
''' Recursively evaluate the parenthesized expression '''
nonlocal i
nonlocal tokens
depth = 1
# Check for empty parentheses
if tokens[i+1] == ")":
return None
# Find the end of the parenthesized expression and count
# the depth of the parentheses to catch nested parentheses
depth = 1
i += 1
start = i
while True:
if i >= len(tokens):
# Check for mismatched parentheses. This should never happen because we check for mismatched parentheses in tokenize()
eprint("Invalid expression: mismatched parentheses")
return None
if tokens[i] == "(":
depth += 1
elif tokens[i] == ")":
depth -= 1
if depth == 0:
break
i += 1
# Recursively evaluate the parenthesized expression
return evaluate_tokens(tokens[start:i])
# Check for empty expression
if len(tokens) == 0:
return None
# Check for single value expression. Since we already converted variables to their values, this should only be a number
if len(tokens) == 1:
if type(tokens[0]) != int:
eprint("Invalid expression: invlaid token" + str(tokens[0]) + type(tokens[0]))
return None
else:
return tokens[0]
# ~~ LEFT VALUE ~~
# Handle variable assignment
if type(tokens[i]) == str and re.match(pmpv['identifier'], tokens[0]):
if tokens[1] != "=":
eprint("Invalid expression: expected '='")
return None
Variables.get_instance().set(tokens[i], evaluate_tokens(tokens[2:]))
return None
# Another check for invalid syntax
if tokens[i] in ["-", "+"]:
eprint("Invalid expression")
return None
# If the left value is in parenthesis, recursively evaluate the parenthesized expression
if tokens[i] == "(":
left = paren_recurse()
# Otherwise, just set the left value to the token
else:
left = tokens[i]
i += 1
# If the left value is the only value in the expression, return it
if i >= len(tokens):
return left
# ~~ OPERATOR VALUE ~~
operator = tokens[i]
# Make sure the operator is valid
if operator not in ["-", "+"]:
eprint("Invalid expression: expected operator")
return None
i += 1
# ~~ RIGHT VALUE ~~
# If the right value is in parenthesis, recursively evaluate the parenthesized expression
if tokens[i] == "(":
right = paren_recurse()
# Another check for invalid syntax
elif tokens[i] in ["-", "+"]:
eprint("Invalid expression: expression cannot end with operator")
return None
# Otherwise, just set the right value to the token
else:
right = tokens[i]
i += 1
# Check that the left, right, and operator values are valid
if left == None or right == None or operator == None:
eprint("Invalid expression: invalid token")
return None
if type(left) != int or type(right) != int:
eprint("Invalid expression: invalid token")
return None
if operator not in ["-", "+"]:
eprint("Invalid expression: invalid token")
return None
# Calculate the sum of the expression
sum = 0
if operator == "+":
sum = left + right
elif operator == "-":
sum = left - right
else:
eprint("Invalid expression: invalid operator")
return None
# If there are more tokens, recursively evaluate them
if i <= len(tokens):
sum = evaluate_tokens([sum] + tokens[i:])
return sum
def main():
''' Main function to handle user input '''
while True:
try: # Handle EOF
userInput = input("")
tokens = tokenize(userInput) # Tokenize the input
if tokens == None: # If the input is invalid
continue
tokens = evaluate_tokens(tokens) # Evaluate the tokens
if tokens != None: # If the tokens are valid, print the result
print(tokens)
else:
print("")
except EOFError: # Gracefully exit on EOF
break
if __name__ == '__main__':
main()