mirror of
https://github.com/opus-tango/pmpv-python.git
synced 2026-03-20 03:55:22 +00:00
added program file
This commit is contained in:
235
pmpv.py
Normal file
235
pmpv.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user