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 for `NameError` errors and calculates the result of the expression. - The `SmartCalculator` class contains the `SyntacticAnalizer` and `Interpreter` 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][1] 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() [1]: https://github.com/sergeVe/smart-calculator/blob/master/calculator.py