I have implemented a console calculator.
It has support for variables over a set of integers and the following operations: multiplication, integer division, exponentiation, addition and subtraction. You can use parentheses in expressions.
The
SyntacticalAnalizer
class implements parsing and syntactic validation of a string entered by the user, converting the expression into reverse polish notation.The
Interpreter
class checks forNameError
errors and calculates the result of the expression.The
SmartCalculator
class contains theSyntacticAnalizer
andInterpreter
classes and provides the user interface.
The code passed the tests, but the state processing turned out to be very noodle-like. I would be grateful for advice on improving the code architecture. Source on GitHub
from string import ascii_letters
from collections import deque
class SyntacticalAnalyzer:
`"""
The name of a variable (identifier) can contain only Latin letters.
A variable can have a name consisting of more than one letter.
The case is also important; for example, n is not the same as N.
The value can be an integer number or a value of another variable.
Addition and subtraction operations are allowed.
Commands begin with a slash and can be: /exit and /help
"""`
_digits_tags: str = '1234567890'
_commands = ['/exit', '/help']
left_part: str = None
expression_stack: list = []
operator_priority: dict = {
'(': 0,
'+': 2,
'-': 2,
'*': 3,
'/': 3,
'^': 4,
')': 0
}
rpn_stack: deque = deque()
@staticmethod
def check_ascii(name):
for letter in name:
if letter not in ascii_letters:
return False
return True
@property
def checked_string(self) -> str:
return self.__checked_string
@checked_string.setter
def checked_string(self, value: str):
self.__checked_string = value
@property
def check_result(self):
return self.res
def __init__(self):
self.__checked_string: str = ''
self._state: str = 'assignment operator'
# scan chain bypass rules
self.chain_rules: dict = dict(skip=False,
off=False
)
# check status
self._status = dict(checker='',
error=None,
check_res=False,
)
# object passed to the wrapper class
self.res = dict(error=None,
state=None,
command=None,
left=None,
rpn_expression=None
)
# list of test functions
self._check_chain: list = [self.check_not_empty,
self.check_command_tag,
self.check_command_incorrectness,
self.check_equality_tag,
self.check_left_part,
self.check_right_part,
self.to_rpn
]
def notify(self, checker: str, check_res: bool):
"""
Passes the function name and the result of its work to the self._status object
@param checker: name of the function passed
@type checker: str
@param check_res: result of the function passed
@type check_res: bool
@return: None
"""
self._status['checker'] = checker
self._status['check_res'] = check_res
def check_status_handler(self):
"""
Reads the modified self._status object, sets self._state
and modifies the self.chain_rules object
@return: None
"""
if self._status['checker'] == 'check_not_empty' and not self._status['check_res']:
self._status['error'] = 'empty'
self._state = 'empty'
if self._status['checker'] == 'check_command_tag':
if self._status['check_res']:
self._state = 'command'
else:
self.chain_rules['skip'] = True
if self._status['checker'] == 'check_command_incorrectness':
if not self._status['check_res']:
self._status['error'] = self.add_command()
self.chain_rules['off'] = True
else:
self.chain_rules['off'] = True
if self._status['checker'] == 'check_equality_tag':
if not self._status['check_res']:
self._state = 'expression'
self.chain_rules['skip'] = True
if self._status['checker'] == 'check_left_part':
if not self._status['check_res']:
self._status['error'] = 'Invalid identifier'
if self._status['checker'] == 'check_right_part':
if not self._status['check_res']:
if self._state == 'assignment operator':
self._status['error'] = 'Invalid assignment'
self.chain_rules['skip'] = True
else:
self._status['error'] = 'Invalid identifier'
self.chain_rules['skip'] = True
if self._status['checker'] == 'to_rpn':
if not self._status['check_res']:
if self._state == 'assignment operator':
self._status['error'] = 'Invalid assignment'
else:
self._status['error'] = 'Invalid expression'
def perform_res(self):
"""
Checks self._state and self._status. Fills out the dictionary self.res
@return: None
"""
self.res['state'] = self._state
if self._state == 'empty':
self.res['error'] = 'empty'
self.chain_rules['off'] = True
if self._state == 'command':
if self._status['error'] is None:
self.res['command'] = self.add_command()
else:
self.res['error'] = self._status['error']
if self._state == 'assignment operator':
if self._status['error'] is None:
self.res['left'] = self.left_part
self.res['rpn_expression'] = self.rpn_stack
else:
self.res['error'] = self._status['error']
if self._state == 'expression':
if self._status['error'] is None:
self.res['rpn_expression'] = self.rpn_stack
else:
self.res['error'] = self._status['error']
def clear_init_fields(self):
"""
Clears all constructor fields before checking for a new line
@return: None
"""
self._state = 'assignment operator'
self._status['checker'] = ''
self._status['error'] = None
self._status['check_res'] = False
self.chain_rules['skip'] = False
self.chain_rules['off'] = False
for key, value in self.res.items():
if type(value) != dict:
self.res[key] = None
self.expression_stack = []
def run_check_chain(self):
"""
Starts a string check chain. Reads objects self.chain_rules and
self._status, if the skip == True property skips the next check,
if the property off == True or one of the checks has completed
with an error, terminates its work
@return: None
"""
j = -1
self.clear_init_fields()
for i, check in enumerate(self._check_chain):
if j == i:
self.chain_rules['skip'] = False
if self.chain_rules['skip']:
j = i + 1 if i + 1 < len(self._check_chain) else -1
continue
self.run_check(check)
self.check_status_handler()
if self._status['error'] is not None:
break
if self.chain_rules['off']:
break
self.perform_res()
def run_check(self, check_func):
"""
@type check_func: function
"""
result = check_func()
self.notify(check_func.__name__, result)
def check_not_empty(self):
return self.checked_string != ''
def check_command_tag(self):
return self.checked_string.startswith('/')
def check_command_incorrectness(self):
return self.checked_string in self._commands
def add_command(self) -> str:
for _command in self._commands:
if self.checked_string == _command:
return _command
return 'Unknown command'
def check_equality_tag(self) -> bool:
return '=' in self.checked_string
def is_variable(self, name: str) -> bool:
return all([len(name) >= 1, self.check_ascii(name)])
def check_left_part(self):
if self._state == 'assignment operator':
self.left_part = self.checked_string.split('=')[0].strip()
return self.is_variable(self.left_part)
@staticmethod
def get_fragment_params(value: str, end):
out_str = ''
pos = 0
sym = value[0]
while sym not in end:
out_str += sym
try:
pos += 1
sym = value[pos]
except IndexError:
return out_str, None
return out_str, pos
@staticmethod
def is_operator(item: str):
item_list: list = item.strip().split(' ')
my_str = ''.join(item_list)
if my_str[0] in '+-':
for el in my_str:
if el not in '+-':
return False
if my_str[0] in '/*^':
if len(my_str) > 1:
return False
return True
@staticmethod
def is_digit(item: str):
if item[0] == '0':
if len(item) != 1:
return False
return True
for el in item:
if el not in '1234567890':
return False
return True
@staticmethod
def is_left_parenthesis(item: str):
for el in item:
if el not in '(':
return False
return True
@staticmethod
def is_right_parenthesis(item: str):
for el in item:
if el not in ')':
return False
return True
@staticmethod
def get_first(value: str):
return value[0] if value else None
@staticmethod
def get_tag(letter: str):
if letter in ascii_letters:
return 'variable'
if letter in '-+/*^':
return 'operator'
if letter in '1234567890':
return 'digit'
if letter in '()':
return 'left parenthesis' if letter == '(' else 'right parenthesis'
@staticmethod
def get_end_tag(tag: str) -> str:
if tag == 'variable':
return ' )+-/*^'
if tag == 'operator':
return '(0123456789' + ascii_letters
if tag == 'digit':
return ' )+-/*^'
if tag == 'left parenthesis':
return ' 0123456789' + ascii_letters + '+-'
if tag == 'right parenthesis':
return ' +-/*^'
@staticmethod
def transform_operator(el: str):
if '-' in el or '+' in el:
minus_cnt = el.count('-')
if minus_cnt:
return '-' if minus_cnt % 2 != 0 else '+'
return '+'
return el
@staticmethod
def transform_parenthesis(el: str):
return list(el)
def transform_element(self, el: str, tag: str):
if tag == 'operator':
return self.transform_operator(el)
if tag in ['left parenthesis', 'right parenthesis']:
return self.transform_parenthesis(el.rstrip())
return el.rstrip()
@staticmethod
def add_el(container: list, el):
if type(el) == list:
container += el
else:
container.append(el.rstrip())
def check_right_part(self):
next_pos = 0
if self._state == 'assignment operator':
input_str = self.checked_string.split('=', 1)[1].strip()
else:
input_str = self.checked_string.strip()
if not input_str:
return False
while True:
current: str = input_str[next_pos:]
sym: str = self.get_first(current)
name = self.get_tag(sym)
end_tag = self.get_end_tag(name)
el, offset = self.get_fragment_params(value=current, end=end_tag)
conditions = [
self.is_variable(el),
self.is_operator(el),
self.is_digit(el),
self.is_left_parenthesis(el),
self.is_right_parenthesis(el)
]
if not any(conditions):
return False
el = self.transform_element(el=el, tag=name)
if not self.expression_stack or self.expression_stack[-1] == '(':
if el in '+-':
self.expression_stack.append('0')
self.add_el(self.expression_stack, el)
if offset is None:
return True
temp = current[offset:]
offset += temp.find(temp.lstrip())
next_pos += offset
def to_rpn(self):
f = False
operators: list = []
for item in self.expression_stack:
if self.is_digit(item) or self.is_variable(item):
self.rpn_stack.append(item)
else:
if not operators:
operators.append(item)
else:
if item == '(' or self.operator_priority[item] > self.operator_priority[operators[-1]]:
operators.append(item)
else:
if not operators:
return False
while operators:
operator = operators.pop()
if operator == '(':
f = True
break
self.rpn_stack.append(operator)
if item == ')' and not f:
return False
if item != ')':
operators.append(item)
if operators:
if '(' in operators:
return False
else:
while operators:
self.rpn_stack.append(operators.pop())
return True
# End of class SyntacticalAnalyzer
class Interpreter:
bye_string = 'Bye!'
help_string = 'The program calculates expressions using addition, subtraction, multiplication, integer division' \
' and exponentiation over a set of integers, and also uses variables.'
def __init__(self, obj):
self.variables: dict = {}
self.obj = obj
self.error: str = None
self.res: int = None
self.rpn_stack: deque = deque()
def execute(self):
if not self.analysis_handler():
return False
return True
def analysis_handler(self):
"""
Читает self.obj.
@return:
"""
self.rpn_stack = deque()
self.res = None
self.error = None
if self.obj['state'] == 'empty':
pass
if self.obj['state'] == 'command':
if not self.command_handler(self.obj['command']):
return False
if self.obj['state'] == 'expression':
if not self.expression_handler():
print(self.error)
else:
print(self.res)
if self.obj['state'] == 'assignment operator':
if not self.assignment_handler():
print(self.error)
return True
def command_handler(self, param: str) -> bool:
if param == '/exit':
print(self.bye_string)
return False
if param == '/help':
print(self.help_string)
return True
def expression_handler(self):
if not self.check_variables():
return False
self.res = self.get_expression_result()
return True
@staticmethod
def calculate_this(one, two, sign):
one, two = [int(x) for x in [one, two]]
if sign == '+':
return one + two
if sign == '-':
return one - two
if sign == '*':
return one * two
if sign == '/':
return one // two
if sign == '^':
return one ** two
@staticmethod
def is_digit(item: str):
if item[0] == '0':
if len(item) != 1:
return False
return True
for el in item:
if el not in '1234567890':
return False
return True
def get_expression_result(self):
result_stack: list = []
while self.rpn_stack:
item = self.rpn_stack.popleft()
if self.is_digit(item):
result_stack.append(item)
else:
second, first = result_stack.pop(), result_stack.pop()
result_stack.append(self.calculate_this(first, second, item))
return result_stack[0]
def assignment_handler(self):
if not self.expression_handler():
return False
left = self.obj['left']
self.variables[left] = self.res
return True
def check_variables(self):
self.rpn_stack = self.obj['rpn_expression']
for i, item in enumerate(self.rpn_stack):
if item in self.variables:
self.rpn_stack[i] = self.variables[item]
else:
if item[0] in ascii_letters:
return False
return True
class SmartCalculator:
"""
The name of a variable (identifier) can contain only Latin letters.
A variable can have a name consisting of more than one letter.
The case is also important; for example, n is not the same as N.
The value can be an integer number or a value of another variable.
It should be possible to set a new value to an existing variable.
To print the value of a variable you should just type its name.
"""
_analyzer_methods = ['run_check_chain']
_interpreter_methods = ['execute', 'analysis_handler']
def __init__(self):
self._analyzer: SyntacticalAnalyzer = SyntacticalAnalyzer()
self.analyzer_result: dict = self._analyzer.check_result
self._interpreter: Interpreter = Interpreter(self.analyzer_result)
def __getattr__(self, item):
for item in self._analyzer_methods + self._interpreter_methods:
if item in self._analyzer_methods:
return getattr(self._analyzer, item)
if item in self._interpreter_methods:
return getattr(self._interpreter, item)
def run(self):
while True:
self._analyzer.checked_string = input().strip()
self._analyzer.run_check_chain()
if self._analyzer.res['error'] is not None and self._analyzer.res['error'] != 'empty':
print(self._analyzer.res['error'])
else:
if not self._interpreter.execute():
return None
calculator = SmartCalculator()
calculator.run()