From 42a4ee9257c6348e2d45f0741da6ce76988acafe Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Wed, 25 Mar 2020 17:48:42 +0200 Subject: [PATCH 01/15] Remove spaces and operators structure --- calculator/calculator.py | 6 +++++ calculator/operators.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 calculator/calculator.py create mode 100644 calculator/operators.py diff --git a/calculator/calculator.py b/calculator/calculator.py new file mode 100644 index 0000000..6a3b4ec --- /dev/null +++ b/calculator/calculator.py @@ -0,0 +1,6 @@ +"""Advanced calculator interpreter.""" + + +def remove_spaces(expression: str) -> str: + """Remove the additional spaces from a given expression.""" + return ''.join(expression.split()) diff --git a/calculator/operators.py b/calculator/operators.py new file mode 100644 index 0000000..9da4248 --- /dev/null +++ b/calculator/operators.py @@ -0,0 +1,50 @@ +"""The operators supported by the calculator.""" + +import math +import operator + + +class Operator: + """Representation of a single mathematical operator. + + Attributes: + operation (function): the function that does the operation. + precedence (int): the priority of the operation. + + Note: + Precedence determines which operation goes after another. + """ + + def __init__(self, operation, precedence): + self.operation = operation + self.precedence = precedence + + +def average(num1, num2): + """Return the average of two numbers (int/float).""" + return float(num1 + num2) / 2.0 + + +"""A dictionary containing the information about supported operations.""" +operators = { + '+': Operator(operator.add, 1), + '-': Operator(operator.sub, 1), + + '*': Operator(operator.mul, 2), + '/': Operator(operator.truediv, 2), + + '^': Operator(math.pow, 3), + '%': Operator(math.fmod, 4), + + '@': Operator(average, 5), + '$': Operator(max, 5), + '&': Operator(min, 5), + + '~': Operator(operator.neg, 6), + '!': Operator(math.factorial, 7), +} + + +def is_operator(symbol: str) -> bool: + """Return true if operation is supported else false.""" + return symbol in operators.keys() From c3a57bac5118ddbcef384aa9caa9ea700bc5f562 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Sun, 29 Mar 2020 18:49:13 +0300 Subject: [PATCH 02/15] Added inner parentheses function --- calculator/calculator.py | 39 ++++++++++++++++++++++++++++++++++++++- calculator/operators.py | 3 +++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 6a3b4ec..b5bfd1c 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -1,6 +1,43 @@ """Advanced calculator interpreter.""" +import re -def remove_spaces(expression: str) -> str: +from operators import operators + + +def _remove_spaces(expression: str) -> str: """Remove the additional spaces from a given expression.""" return ''.join(expression.split()) + + +# def _innermost_parentheses(text: str) -> str: +# """Return the text inside the innermost parenthesis.""" +# if all(['(' not in text, ')' not in text]): +# return text +# +# open_parentheses = text.index('(') +# close_parentheses = text.index(')') +# return _innermost_parentheses(text[open_parentheses + 1:close_parentheses]) + + +class Parser: + """Advanced calculator class.""" + + def __init__(self, expression: str): + self.expression = _remove_spaces(expression) + + def _split(self): + supported_operators = ','.join(operators.keys()) + negative_positive_ints = r'[+-]?\d+' + negative_positive_floats = negative_positive_ints + r'\.?\d+' + + number_or_symbol = re.compile( + rf'([{supported_operators}]|{negative_positive_floats}|{negative_positive_ints})') + + return re.findall(number_or_symbol, self.expression) + + +# source = '((81 * 6) /42+ (3-1))' +source = '((81 * 6) /42+ (3.5-1))' +c = Parser(source) +print(c._split()) diff --git a/calculator/operators.py b/calculator/operators.py index 9da4248..c55918e 100644 --- a/calculator/operators.py +++ b/calculator/operators.py @@ -42,6 +42,9 @@ def average(num1, num2): '~': Operator(operator.neg, 6), '!': Operator(math.factorial, 7), + + '(': Operator(None, 8), + ')': Operator(None, 8) } From eafc74c6d55a53b41fa28c0b83ca3c5e2230468e Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Tue, 31 Mar 2020 13:38:24 +0300 Subject: [PATCH 03/15] Different methods for most inner parentheses --- calculator/calculator.py | 68 +++++++++++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index b5bfd1c..829b74a 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -10,14 +10,56 @@ def _remove_spaces(expression: str) -> str: return ''.join(expression.split()) -# def _innermost_parentheses(text: str) -> str: -# """Return the text inside the innermost parenthesis.""" -# if all(['(' not in text, ')' not in text]): -# return text -# -# open_parentheses = text.index('(') -# close_parentheses = text.index(')') -# return _innermost_parentheses(text[open_parentheses + 1:close_parentheses]) +def _innermost_parentheses(text: list) -> list: + """Return the text inside the innermost parenthesis.""" + if all(['(' not in text, ')' not in text]): + return text + + print(text) + + open_paren = text.index('(') + close_paren = text.index(')') + + print(open_paren, close_paren) + print(text) + + return _innermost_parentheses(text[open_paren + 1:close_paren]) + + +def max_depth(s): + current_max = 0 + max = 0 + n = len(s) + + # Traverse the input string + for i in range(n): + if s[i] == '(': + current_max += 1 + + if current_max > max: + max = current_max + elif s[i] == ')': + if current_max > 0: + current_max -= 1 + else: + return -1 + + # finally check for unbalanced string + if current_max != 0: + return -1 + + return max + + +def parenthetic_contents(string): + """Generate parenthesized contents in string as pairs (level, contents).""" + stack = [] + for i, c in enumerate(string): + if c == '(': + stack.append(i) + elif c == ')' and stack: + start = stack.pop() + yield len(stack), string[start + 1: i] class Parser: @@ -26,13 +68,14 @@ class Parser: def __init__(self, expression: str): self.expression = _remove_spaces(expression) - def _split(self): + def split(self): supported_operators = ','.join(operators.keys()) negative_positive_ints = r'[+-]?\d+' negative_positive_floats = negative_positive_ints + r'\.?\d+' number_or_symbol = re.compile( - rf'([{supported_operators}]|{negative_positive_floats}|{negative_positive_ints})') + rf'([{supported_operators}]|{negative_positive_floats}' + rf'|{negative_positive_ints})') return re.findall(number_or_symbol, self.expression) @@ -40,4 +83,7 @@ def _split(self): # source = '((81 * 6) /42+ (3-1))' source = '((81 * 6) /42+ (3.5-1))' c = Parser(source) -print(c._split()) +print(c.split()) +# print(_innermost_parentheses(c.split())) +print(max_depth(c.split())) +print(list(parenthetic_contents(source))) From eb5061a5bfa393e03f826fc1318bffb5a81edfc3 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Mon, 6 Apr 2020 13:18:57 +0300 Subject: [PATCH 04/15] before debugging --- calculator/calculator.py | 142 +++++++++++++++---------- calculator/calculator_sandbox.py | 175 +++++++++++++++++++++++++++++++ calculator/operators.py | 2 +- 3 files changed, 261 insertions(+), 58 deletions(-) create mode 100644 calculator/calculator_sandbox.py diff --git a/calculator/calculator.py b/calculator/calculator.py index 829b74a..2802f94 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -5,85 +5,113 @@ from operators import operators -def _remove_spaces(expression: str) -> str: - """Remove the additional spaces from a given expression.""" - return ''.join(expression.split()) +def expression_by_inner_parentheses(total_expression: str, + parsed: list) -> list: + """Parse the given expression by parentheses by most inner parentheses. + + Args: + total_expression: the expression to parse. + parsed: list of all parsed expression items, by order of precedence. + + Returns: + A list of expression items ordered by most inner parentheses. + """ + non_nested_parentheses = r'\(([^(\)]*)\)' + + if '(' not in total_expression: + return parsed + + current_inner_parentheses = re.findall(non_nested_parentheses, + total_expression) + + parsed.extend(current_inner_parentheses) + + total_expression = re.sub(non_nested_parentheses, '', + total_expression) # removes already found + + return expression_by_inner_parentheses(total_expression, parsed) + +def evaluate_parentheses(expression: str, parsed_expression: list) -> str: + if not parsed_expression: + return expression -def _innermost_parentheses(text: list) -> list: - """Return the text inside the innermost parenthesis.""" - if all(['(' not in text, ')' not in text]): - return text + current_sub_expression = parsed_expression.pop() - print(text) + operator, operator_index = find_operator_in_sub_expression( + current_sub_expression) - open_paren = text.index('(') - close_paren = text.index(')') + if not operator and operator_index == -1: + return expression - print(open_paren, close_paren) - print(text) + operand1, operand2 = current_sub_expression.split(operator) - return _innermost_parentheses(text[open_paren + 1:close_paren]) + operation_result = evaluate_sub_expression(operator, operand1, operand2) + new_expression = expression.replace(f"({current_sub_expression})", + str(operation_result)) -def max_depth(s): - current_max = 0 - max = 0 - n = len(s) + return evaluate_parentheses(new_expression, parsed_expression) - # Traverse the input string - for i in range(n): - if s[i] == '(': - current_max += 1 - if current_max > max: - max = current_max - elif s[i] == ')': - if current_max > 0: - current_max -= 1 - else: - return -1 +def evaluate_sub_expression(operator_symbol, left_operand, right_operand): + if left_operand and right_operand: + return operators[operator_symbol].operation(float(left_operand), + float(right_operand)) - # finally check for unbalanced string - if current_max != 0: - return -1 + elif not left_operand and right_operand: + return operators[operator_symbol].operation(float(right_operand)) - return max + elif not right_operand and left_operand: + return operators[operator_symbol].operation(float(left_operand)) -def parenthetic_contents(string): - """Generate parenthesized contents in string as pairs (level, contents).""" - stack = [] - for i, c in enumerate(string): - if c == '(': - stack.append(i) - elif c == ')' and stack: - start = stack.pop() - yield len(stack), string[start + 1: i] +def find_operator_in_sub_expression(sub_expression: str) -> str: + for potential_operator in operators: + try: + operator_index = sub_expression.find(potential_operator) + return sub_expression[operator_index] + except ValueError: + continue -class Parser: + return "" + + +def remove_outer_parentheses(expression: str) -> str: + if all([expression[0] == "(", expression[-1] == ")"]): + return expression[1:-1] + + +def remove_spaces(expression: str) -> str: + return ''.join(expression.split()) + + +class Calculator: """Advanced calculator class.""" def __init__(self, expression: str): - self.expression = _remove_spaces(expression) + self.expression = remove_outer_parentheses(remove_spaces(expression)) + + def _parse(self, formula): + parsed_expression = expression_by_inner_parentheses(formula, []) + + return list(filter(None, (expr.strip() for expr in + parsed_expression)))[::-1] # low to high - def split(self): - supported_operators = ','.join(operators.keys()) - negative_positive_ints = r'[+-]?\d+' - negative_positive_floats = negative_positive_ints + r'\.?\d+' + def evaluate(self): + parentheses_calculation = evaluate_parentheses( + self.expression, self._parse(self.expression)) - number_or_symbol = re.compile( - rf'([{supported_operators}]|{negative_positive_floats}' - rf'|{negative_positive_ints})') + print(parentheses_calculation) - return re.findall(number_or_symbol, self.expression) + while "(" in parentheses_calculation: + parentheses_calculation = evaluate_parentheses( + self.expression, self._parse(parentheses_calculation)) + print(parentheses_calculation) # source = '((81 * 6) /42+ (3-1))' -source = '((81 * 6) /42+ (3.5-1))' -c = Parser(source) -print(c.split()) -# print(_innermost_parentheses(c.split())) -print(max_depth(c.split())) -print(list(parenthetic_contents(source))) +source = '((81 * 6) /42 $ (5*(3.5 @ 7)-1))' +c = Calculator(source) +print(c.evaluate()) diff --git a/calculator/calculator_sandbox.py b/calculator/calculator_sandbox.py new file mode 100644 index 0000000..6b97225 --- /dev/null +++ b/calculator/calculator_sandbox.py @@ -0,0 +1,175 @@ +import re + +from operators import operators, is_operator + + +def remove_spaces(expression: str) -> str: + """Remove the spaces between characters in a string.""" + return ''.join(expression.split()) + + +def get_inner_parentheses(expression: str) -> str: + """Return the most inner parentheses in an expression.""" + pattern = re.compile(r"\([^()]*\)") + without_outer_parentheses = [inner_expr[1:-1] for inner_expr in + pattern.findall(expression)] + if without_outer_parentheses: + return without_outer_parentheses[-1] + + return expression + + +def calculate_sub_expression(operator_symbol, left_operand, right_operand): + """Evaluate an expression. + + Args: + left_operand (str): the left operand. + right_operand (str): the right operand. + operator_symbol (str): a symbol represents the mathematical operation. + + Returns: + A float, the result of the mathematical operation. + """ + if left_operand and right_operand: + return operators[operator_symbol].operation(float(left_operand), + float(right_operand)) + + elif not left_operand and right_operand: + return operators[operator_symbol].operation(float(right_operand)) + + elif not right_operand and left_operand: + return operators[operator_symbol].operation(float(left_operand)) + + +def get_highest_precedence_operator(expression: str) -> str: + """Return the highest priority operator in a given expression.""" + without_operands = filter_operands_out(expression) + return sorted(without_operands, key=lambda ch: operators[ch].precedence, + reverse=True)[0] + + +def filter_operands_out(sub_expression: str) -> list: + """Remove operands from sub expression, leaves only operators.""" + return [ch for ch in sub_expression if is_operator(ch)] + + +def parse_sub_expression_by_operator(sub_expression, operator): + """Analyze a sub expression to its operands, by its operator. + + Args: + sub_expression (str): the expression to analyze. + operator (str): the operator of the sub expression. + + Returns: + A tuple of both operands, left and right. + """ + any_int_or_float = r"-?\d+(\.\d+)?" + + pattern = re.compile(rf"(?P{any_int_or_float})?\{operator}(" + rf"?P{any_int_or_float})?") + + matches = pattern.search(sub_expression).groupdict() + + return matches["left_operand"], matches["right_operand"] + + +def has_operations(expression: str) -> bool: + """Return True if the expression has operations in it else, false.""" + return any([True if is_operator(ch) else False for ch in expression]) + + +def is_number(s: str) -> bool: + """Return true if a string can be a number else, false.""" + try: + num = float(s) + + except ValueError: + return False + + return True + + +def parse_sub_expression(sub_expr: str) -> tuple: + """Parse an expression according to its highest priority operator. + + Args: + sub_expr: the part of the expression to parsed. + + Returns: + The left operand, the operator and the right operand. + + Note: + The left operand or right operand can be "" if the operator is unary. + """ + operator = get_highest_precedence_operator(sub_expr) + + left_operand, right_operand = parse_sub_expression_by_operator( + sub_expr, operator) + + return left_operand, right_operand, operator + + +def replace_in_expression(old_expr, old_sub_expr, new_sub_expr): + """Replace sub expression in the entire expression. + + Args: + old_expr (str): the entire expression. + old_sub_expr (str): the expression to be replaced. + new_sub_expr (Str): the new expression to be put in. + + Returns: + str: the new entire expression. + """ + if is_number(new_sub_expr): + return old_expr.replace(f"({old_sub_expr})", new_sub_expr) + + return old_expr.replace(f"{old_sub_expr}", new_sub_expr) + + +def check_operands(func): + def wrapper(operator_symbol, left_operand, right_operand): + if left_operand and right_operand: + func(operator_symbol, ) + + elif not left_operand and right_operand: + func() + + elif not right_operand and left_operand: + func() + + return wrapper + + +def evaluate(expression: str) -> str: + """Calculates the value of a mathematical expression.""" + print(expression) + sub_expression = get_inner_parentheses(expression) + + if not has_operations(expression): + return sub_expression + + left_operand, right_operand, current_operator = parse_sub_expression( + sub_expression) + + current_result = calculate_sub_expression(current_operator, left_operand, + right_operand) + + updated_sub_expression = sub_expression.replace( + f"{left_operand}{current_operator}{right_operand}", + str(current_result)) + + print(updated_sub_expression) + + expression = replace_in_expression(expression, sub_expression, + updated_sub_expression) + + return evaluate(expression) + + +if __name__ == '__main__': + # source = '(((81 * 6) /42 $ (5*(3.5 @ 7)-1)))' + # source = "(100^0.5)" + # source = "(1 + (3*(3^0.5)))" + source = "(3! + 3^(2!))" + + print(evaluate(remove_spaces(source))) diff --git a/calculator/operators.py b/calculator/operators.py index c55918e..19c75ef 100644 --- a/calculator/operators.py +++ b/calculator/operators.py @@ -50,4 +50,4 @@ def average(num1, num2): def is_operator(symbol: str) -> bool: """Return true if operation is supported else false.""" - return symbol in operators.keys() + return symbol in operators From 8a27fe33535362c51158dc121c21686bc015df43 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Mon, 20 Apr 2020 12:18:18 +0300 Subject: [PATCH 05/15] First working stable version --- calculator/calculator_sandbox.py | 85 ++++++++++++++++---------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/calculator/calculator_sandbox.py b/calculator/calculator_sandbox.py index 6b97225..094f855 100644 --- a/calculator/calculator_sandbox.py +++ b/calculator/calculator_sandbox.py @@ -8,37 +8,62 @@ def remove_spaces(expression: str) -> str: return ''.join(expression.split()) +def check_operands(func): + """Check the existence of the operands. + + Args: + func (function): the function to operate on. + + Returns: + function: the function wrapper. + """ + + def operands_wrapper(operator_symbol, operands): + left_operand, right_operand = operands + + if left_operand and right_operand: + return func(operator_symbol, operands) + + elif not left_operand and right_operand: + if any([operator_symbol == "+", operator_symbol == "-"]): + return float(f"{operator_symbol}{right_operand}") + + return func(operator_symbol, [right_operand]) + + elif not right_operand and left_operand: + return func(operator_symbol, [left_operand]) + + return operands_wrapper + + def get_inner_parentheses(expression: str) -> str: """Return the most inner parentheses in an expression.""" pattern = re.compile(r"\([^()]*\)") without_outer_parentheses = [inner_expr[1:-1] for inner_expr in pattern.findall(expression)] + if without_outer_parentheses: return without_outer_parentheses[-1] return expression -def calculate_sub_expression(operator_symbol, left_operand, right_operand): +@check_operands +def calculate_sub_expression(operator_symbol, operands): """Evaluate an expression. Args: - left_operand (str): the left operand. - right_operand (str): the right operand. + operands (list): the operands, can be 2 or 1 according to operation. operator_symbol (str): a symbol represents the mathematical operation. Returns: A float, the result of the mathematical operation. """ - if left_operand and right_operand: - return operators[operator_symbol].operation(float(left_operand), - float(right_operand)) - - elif not left_operand and right_operand: - return operators[operator_symbol].operation(float(right_operand)) + if len(operands) == 2: + return operators[operator_symbol].operation(float(operands[0]), + float(operands[1])) - elif not right_operand and left_operand: - return operators[operator_symbol].operation(float(left_operand)) + return operators[operator_symbol].operation(float(operands[0])) def get_highest_precedence_operator(expression: str) -> str: @@ -69,6 +94,8 @@ def parse_sub_expression_by_operator(sub_expression, operator): rf"?P{any_int_or_float})?") matches = pattern.search(sub_expression).groupdict() + # replace None with "" + matches = {k: ("" if not v else v) for k, v in matches.items()} return matches["left_operand"], matches["right_operand"] @@ -126,50 +153,24 @@ def replace_in_expression(old_expr, old_sub_expr, new_sub_expr): return old_expr.replace(f"{old_sub_expr}", new_sub_expr) -def check_operands(func): - def wrapper(operator_symbol, left_operand, right_operand): - if left_operand and right_operand: - func(operator_symbol, ) - - elif not left_operand and right_operand: - func() - - elif not right_operand and left_operand: - func() - - return wrapper - - def evaluate(expression: str) -> str: """Calculates the value of a mathematical expression.""" - print(expression) - sub_expression = get_inner_parentheses(expression) + if is_number(expression): + return expression - if not has_operations(expression): - return sub_expression + sub_expression = get_inner_parentheses(expression) left_operand, right_operand, current_operator = parse_sub_expression( sub_expression) - current_result = calculate_sub_expression(current_operator, left_operand, - right_operand) + current_result = calculate_sub_expression(current_operator, + [left_operand, right_operand]) updated_sub_expression = sub_expression.replace( f"{left_operand}{current_operator}{right_operand}", str(current_result)) - print(updated_sub_expression) - expression = replace_in_expression(expression, sub_expression, updated_sub_expression) return evaluate(expression) - - -if __name__ == '__main__': - # source = '(((81 * 6) /42 $ (5*(3.5 @ 7)-1)))' - # source = "(100^0.5)" - # source = "(1 + (3*(3^0.5)))" - source = "(3! + 3^(2!))" - - print(evaluate(remove_spaces(source))) From f0587c56513408e7a627976433c7e303c0fe1e99 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Mon, 20 Apr 2020 12:33:17 +0300 Subject: [PATCH 06/15] Deleted sandbox --- calculator/calculator.py | 203 ++++++++++++++++++++----------- calculator/calculator_sandbox.py | 176 --------------------------- 2 files changed, 131 insertions(+), 248 deletions(-) delete mode 100644 calculator/calculator_sandbox.py diff --git a/calculator/calculator.py b/calculator/calculator.py index 2802f94..094f855 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -1,117 +1,176 @@ -"""Advanced calculator interpreter.""" - import re -from operators import operators +from operators import operators, is_operator + + +def remove_spaces(expression: str) -> str: + """Remove the spaces between characters in a string.""" + return ''.join(expression.split()) -def expression_by_inner_parentheses(total_expression: str, - parsed: list) -> list: - """Parse the given expression by parentheses by most inner parentheses. +def check_operands(func): + """Check the existence of the operands. Args: - total_expression: the expression to parse. - parsed: list of all parsed expression items, by order of precedence. + func (function): the function to operate on. Returns: - A list of expression items ordered by most inner parentheses. + function: the function wrapper. """ - non_nested_parentheses = r'\(([^(\)]*)\)' - if '(' not in total_expression: - return parsed + def operands_wrapper(operator_symbol, operands): + left_operand, right_operand = operands - current_inner_parentheses = re.findall(non_nested_parentheses, - total_expression) + if left_operand and right_operand: + return func(operator_symbol, operands) - parsed.extend(current_inner_parentheses) + elif not left_operand and right_operand: + if any([operator_symbol == "+", operator_symbol == "-"]): + return float(f"{operator_symbol}{right_operand}") - total_expression = re.sub(non_nested_parentheses, '', - total_expression) # removes already found + return func(operator_symbol, [right_operand]) - return expression_by_inner_parentheses(total_expression, parsed) + elif not right_operand and left_operand: + return func(operator_symbol, [left_operand]) + return operands_wrapper -def evaluate_parentheses(expression: str, parsed_expression: list) -> str: - if not parsed_expression: - return expression - current_sub_expression = parsed_expression.pop() +def get_inner_parentheses(expression: str) -> str: + """Return the most inner parentheses in an expression.""" + pattern = re.compile(r"\([^()]*\)") + without_outer_parentheses = [inner_expr[1:-1] for inner_expr in + pattern.findall(expression)] - operator, operator_index = find_operator_in_sub_expression( - current_sub_expression) + if without_outer_parentheses: + return without_outer_parentheses[-1] - if not operator and operator_index == -1: - return expression + return expression - operand1, operand2 = current_sub_expression.split(operator) - operation_result = evaluate_sub_expression(operator, operand1, operand2) +@check_operands +def calculate_sub_expression(operator_symbol, operands): + """Evaluate an expression. - new_expression = expression.replace(f"({current_sub_expression})", - str(operation_result)) + Args: + operands (list): the operands, can be 2 or 1 according to operation. + operator_symbol (str): a symbol represents the mathematical operation. - return evaluate_parentheses(new_expression, parsed_expression) + Returns: + A float, the result of the mathematical operation. + """ + if len(operands) == 2: + return operators[operator_symbol].operation(float(operands[0]), + float(operands[1])) + return operators[operator_symbol].operation(float(operands[0])) -def evaluate_sub_expression(operator_symbol, left_operand, right_operand): - if left_operand and right_operand: - return operators[operator_symbol].operation(float(left_operand), - float(right_operand)) - elif not left_operand and right_operand: - return operators[operator_symbol].operation(float(right_operand)) +def get_highest_precedence_operator(expression: str) -> str: + """Return the highest priority operator in a given expression.""" + without_operands = filter_operands_out(expression) + return sorted(without_operands, key=lambda ch: operators[ch].precedence, + reverse=True)[0] - elif not right_operand and left_operand: - return operators[operator_symbol].operation(float(left_operand)) +def filter_operands_out(sub_expression: str) -> list: + """Remove operands from sub expression, leaves only operators.""" + return [ch for ch in sub_expression if is_operator(ch)] -def find_operator_in_sub_expression(sub_expression: str) -> str: - for potential_operator in operators: - try: - operator_index = sub_expression.find(potential_operator) - return sub_expression[operator_index] - except ValueError: - continue +def parse_sub_expression_by_operator(sub_expression, operator): + """Analyze a sub expression to its operands, by its operator. - return "" + Args: + sub_expression (str): the expression to analyze. + operator (str): the operator of the sub expression. + Returns: + A tuple of both operands, left and right. + """ + any_int_or_float = r"-?\d+(\.\d+)?" -def remove_outer_parentheses(expression: str) -> str: - if all([expression[0] == "(", expression[-1] == ")"]): - return expression[1:-1] + pattern = re.compile(rf"(?P{any_int_or_float})?\{operator}(" + rf"?P{any_int_or_float})?") + matches = pattern.search(sub_expression).groupdict() + # replace None with "" + matches = {k: ("" if not v else v) for k, v in matches.items()} -def remove_spaces(expression: str) -> str: - return ''.join(expression.split()) + return matches["left_operand"], matches["right_operand"] + + +def has_operations(expression: str) -> bool: + """Return True if the expression has operations in it else, false.""" + return any([True if is_operator(ch) else False for ch in expression]) + + +def is_number(s: str) -> bool: + """Return true if a string can be a number else, false.""" + try: + num = float(s) + except ValueError: + return False -class Calculator: - """Advanced calculator class.""" + return True - def __init__(self, expression: str): - self.expression = remove_outer_parentheses(remove_spaces(expression)) - def _parse(self, formula): - parsed_expression = expression_by_inner_parentheses(formula, []) +def parse_sub_expression(sub_expr: str) -> tuple: + """Parse an expression according to its highest priority operator. + + Args: + sub_expr: the part of the expression to parsed. + + Returns: + The left operand, the operator and the right operand. + + Note: + The left operand or right operand can be "" if the operator is unary. + """ + operator = get_highest_precedence_operator(sub_expr) + + left_operand, right_operand = parse_sub_expression_by_operator( + sub_expr, operator) + + return left_operand, right_operand, operator + + +def replace_in_expression(old_expr, old_sub_expr, new_sub_expr): + """Replace sub expression in the entire expression. + + Args: + old_expr (str): the entire expression. + old_sub_expr (str): the expression to be replaced. + new_sub_expr (Str): the new expression to be put in. + + Returns: + str: the new entire expression. + """ + if is_number(new_sub_expr): + return old_expr.replace(f"({old_sub_expr})", new_sub_expr) + + return old_expr.replace(f"{old_sub_expr}", new_sub_expr) + + +def evaluate(expression: str) -> str: + """Calculates the value of a mathematical expression.""" + if is_number(expression): + return expression - return list(filter(None, (expr.strip() for expr in - parsed_expression)))[::-1] # low to high + sub_expression = get_inner_parentheses(expression) - def evaluate(self): - parentheses_calculation = evaluate_parentheses( - self.expression, self._parse(self.expression)) + left_operand, right_operand, current_operator = parse_sub_expression( + sub_expression) - print(parentheses_calculation) + current_result = calculate_sub_expression(current_operator, + [left_operand, right_operand]) - while "(" in parentheses_calculation: - parentheses_calculation = evaluate_parentheses( - self.expression, self._parse(parentheses_calculation)) + updated_sub_expression = sub_expression.replace( + f"{left_operand}{current_operator}{right_operand}", + str(current_result)) - print(parentheses_calculation) + expression = replace_in_expression(expression, sub_expression, + updated_sub_expression) -# source = '((81 * 6) /42+ (3-1))' -source = '((81 * 6) /42 $ (5*(3.5 @ 7)-1))' -c = Calculator(source) -print(c.evaluate()) + return evaluate(expression) diff --git a/calculator/calculator_sandbox.py b/calculator/calculator_sandbox.py deleted file mode 100644 index 094f855..0000000 --- a/calculator/calculator_sandbox.py +++ /dev/null @@ -1,176 +0,0 @@ -import re - -from operators import operators, is_operator - - -def remove_spaces(expression: str) -> str: - """Remove the spaces between characters in a string.""" - return ''.join(expression.split()) - - -def check_operands(func): - """Check the existence of the operands. - - Args: - func (function): the function to operate on. - - Returns: - function: the function wrapper. - """ - - def operands_wrapper(operator_symbol, operands): - left_operand, right_operand = operands - - if left_operand and right_operand: - return func(operator_symbol, operands) - - elif not left_operand and right_operand: - if any([operator_symbol == "+", operator_symbol == "-"]): - return float(f"{operator_symbol}{right_operand}") - - return func(operator_symbol, [right_operand]) - - elif not right_operand and left_operand: - return func(operator_symbol, [left_operand]) - - return operands_wrapper - - -def get_inner_parentheses(expression: str) -> str: - """Return the most inner parentheses in an expression.""" - pattern = re.compile(r"\([^()]*\)") - without_outer_parentheses = [inner_expr[1:-1] for inner_expr in - pattern.findall(expression)] - - if without_outer_parentheses: - return without_outer_parentheses[-1] - - return expression - - -@check_operands -def calculate_sub_expression(operator_symbol, operands): - """Evaluate an expression. - - Args: - operands (list): the operands, can be 2 or 1 according to operation. - operator_symbol (str): a symbol represents the mathematical operation. - - Returns: - A float, the result of the mathematical operation. - """ - if len(operands) == 2: - return operators[operator_symbol].operation(float(operands[0]), - float(operands[1])) - - return operators[operator_symbol].operation(float(operands[0])) - - -def get_highest_precedence_operator(expression: str) -> str: - """Return the highest priority operator in a given expression.""" - without_operands = filter_operands_out(expression) - return sorted(without_operands, key=lambda ch: operators[ch].precedence, - reverse=True)[0] - - -def filter_operands_out(sub_expression: str) -> list: - """Remove operands from sub expression, leaves only operators.""" - return [ch for ch in sub_expression if is_operator(ch)] - - -def parse_sub_expression_by_operator(sub_expression, operator): - """Analyze a sub expression to its operands, by its operator. - - Args: - sub_expression (str): the expression to analyze. - operator (str): the operator of the sub expression. - - Returns: - A tuple of both operands, left and right. - """ - any_int_or_float = r"-?\d+(\.\d+)?" - - pattern = re.compile(rf"(?P{any_int_or_float})?\{operator}(" - rf"?P{any_int_or_float})?") - - matches = pattern.search(sub_expression).groupdict() - # replace None with "" - matches = {k: ("" if not v else v) for k, v in matches.items()} - - return matches["left_operand"], matches["right_operand"] - - -def has_operations(expression: str) -> bool: - """Return True if the expression has operations in it else, false.""" - return any([True if is_operator(ch) else False for ch in expression]) - - -def is_number(s: str) -> bool: - """Return true if a string can be a number else, false.""" - try: - num = float(s) - - except ValueError: - return False - - return True - - -def parse_sub_expression(sub_expr: str) -> tuple: - """Parse an expression according to its highest priority operator. - - Args: - sub_expr: the part of the expression to parsed. - - Returns: - The left operand, the operator and the right operand. - - Note: - The left operand or right operand can be "" if the operator is unary. - """ - operator = get_highest_precedence_operator(sub_expr) - - left_operand, right_operand = parse_sub_expression_by_operator( - sub_expr, operator) - - return left_operand, right_operand, operator - - -def replace_in_expression(old_expr, old_sub_expr, new_sub_expr): - """Replace sub expression in the entire expression. - - Args: - old_expr (str): the entire expression. - old_sub_expr (str): the expression to be replaced. - new_sub_expr (Str): the new expression to be put in. - - Returns: - str: the new entire expression. - """ - if is_number(new_sub_expr): - return old_expr.replace(f"({old_sub_expr})", new_sub_expr) - - return old_expr.replace(f"{old_sub_expr}", new_sub_expr) - - -def evaluate(expression: str) -> str: - """Calculates the value of a mathematical expression.""" - if is_number(expression): - return expression - - sub_expression = get_inner_parentheses(expression) - - left_operand, right_operand, current_operator = parse_sub_expression( - sub_expression) - - current_result = calculate_sub_expression(current_operator, - [left_operand, right_operand]) - - updated_sub_expression = sub_expression.replace( - f"{left_operand}{current_operator}{right_operand}", - str(current_result)) - - expression = replace_in_expression(expression, sub_expression, - updated_sub_expression) - - return evaluate(expression) From 926a1e5da23328371d9d1fdc70348b5a76b620a5 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Mon, 20 Apr 2020 17:20:12 +0300 Subject: [PATCH 07/15] Added unit tests --- calculator/calculator.py | 47 ++++++++++++++++++++ calculator/test_unittest_calculator.py | 61 ++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 calculator/test_unittest_calculator.py diff --git a/calculator/calculator.py b/calculator/calculator.py index 094f855..d9437e2 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -1,3 +1,19 @@ +"""Calculator module. + +This module allows evaluation of strings which represent mathematical +expressions. The supported operations are declared in operators.py. + +Example: + To use this module, import the evaluate() function: + + from calculator import evaluate + + The parameter of this function is a mathematical expression (str). For + example, "(1 + (3^-2) * (4! - 5$ 2))". Putting parentheses around the + entire expression is necessary. + +""" + import re from operators import operators, is_operator @@ -174,3 +190,34 @@ def evaluate(expression: str) -> str: updated_sub_expression) return evaluate(expression) + + +class Calculator: + """A class which represents a single calculator. + + Args: + expression (str): a mathematical expression to evaluate. + This argument can be with separating spaces. + + Attributes: + expression (str): the mathematical expression to evaluate. + Outer parentheses are added in setter. + + Note: + After the creation of an Calculator instance there is no need to + create a new one to calculate another expression. Just modify the + expression property to the new expression. + """ + def __init__(self, expression=""): + self.expression = expression + + @property + def expression(self): + return self._expression + + @expression.setter + def expression(self, expression): + self._expression = f"({remove_spaces(expression)})" + + def calculate(self): + return float(evaluate(self._expression)) diff --git a/calculator/test_unittest_calculator.py b/calculator/test_unittest_calculator.py new file mode 100644 index 0000000..a027cd7 --- /dev/null +++ b/calculator/test_unittest_calculator.py @@ -0,0 +1,61 @@ +"""Unit test module for testing calculator module.""" + +import unittest + +from calculator import Calculator + + +class TestOperations(unittest.TestCase): + """Test the supported operations by the calculator.""" + def __init__(self, *args, **kwargs): + super(TestOperations, self).__init__(*args, **kwargs) + self.c = Calculator() + + def test_basic(self): + """Test the basic operations.""" + self.c.expression = "1 + 2 + 3" + self.assertEqual(self.c.calculate(), 6.0) + + self.c.expression = "8 - 4 - 6" + self.assertEqual(self.c.calculate(), -2.0) + + self.c.expression = "3* -5" + self.assertEqual(self.c.calculate(), -15.0) + + self.c.expression = "65 /5" + self.assertEqual(self.c.calculate(), 13.0) + + def test_advanced(self): + """Test the advanced operations.""" + self.c.expression = "9 ^ 0.5" + self.assertEqual(self.c.calculate(), 3.0) + + self.c.expression = "3 % 2" + self.assertEqual(self.c.calculate(), 1.0) + + self.c.expression = "15 @ 60" + self.assertEqual(self.c.calculate(), 37.5) + + self.c.expression = "13 $ 16" + self.assertEqual(self.c.calculate(), 16.0) + + self.c.expression = "13 & 16" + self.assertEqual(self.c.calculate(), 13.0) + + self.c.expression = "~9" + self.assertEqual(self.c.calculate(), -9.0) + + self.c.expression = "5 !" + self.assertEqual(self.c.calculate(), 120.0) + + def test_precedence(self): + """Check the order which operations are done.""" + self.c.expression = "1 + 2 * 3 + 4 / 0.5 $ (1 / 3)" + self.assertEqual(self.c.calculate(), 15.0) + + self.c.expression = "((1 - 4)^3)*-1" + self.assertEqual(self.c.calculate(), 27.0) + + +if __name__ == '__main__': + unittest.main() From e4cc0e82eebe3e060789aeeaf690974b8d52a6ec Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Tue, 21 Apr 2020 16:03:49 +0300 Subject: [PATCH 08/15] Finished unit tests and simplification --- calculator/calculator.py | 255 ++++++++++++------------- calculator/operators.py | 53 ----- calculator/test_unittest_calculator.py | 55 ++---- 3 files changed, 142 insertions(+), 221 deletions(-) delete mode 100644 calculator/operators.py diff --git a/calculator/calculator.py b/calculator/calculator.py index d9437e2..13be723 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -1,7 +1,7 @@ """Calculator module. This module allows evaluation of strings which represent mathematical -expressions. The supported operations are declared in operators.py. +expressions. The supported operations are declared in operators dictionary. Example: To use this module, import the evaluate() function: @@ -9,14 +9,42 @@ from calculator import evaluate The parameter of this function is a mathematical expression (str). For - example, "(1 + (3^-2) * (4! - 5$ 2))". Putting parentheses around the - entire expression is necessary. - + example, "1 + (3^-2) * (4! - 5$ 2)". """ import re +import math +import operator +from sys import maxsize + +any_number_regex = r"-?\d+(\.\d+)?" + + +class Operator: + """Representation of a single mathematical operator. + + Attributes: + operation (function): the function that does the operation. + precedence (int): the priority of the operation. + regex (str): regex to find a pattern which suits to the operator. + + Note: + Precedence determines which operation goes after another. + """ + + def __init__(self, operation, precedence, regex): + self.operation = operation + self.precedence = precedence + self.regex = re.compile(regex) + -from operators import operators, is_operator +def average(num1, num2): + """Return the average of two numbers (int/float). + + Note: + Same as using mean() from statistics module. + """ + return float(num1 + num2) / 2.0 def remove_spaces(expression: str) -> str: @@ -24,6 +52,25 @@ def remove_spaces(expression: str) -> str: return ''.join(expression.split()) +def get_sub_expression_by_symbol(symbol: str, expression: str) -> str: + """Find the sub expression the current operator. + + Args: + symbol: the current operator symbol. + expression: the entire expression to search. + + Returns: + The desired sub expression of the operator. If nothing is found + returns an empty string. + """ + match = operators[symbol].regex.search(expression) + + if not match: + return "" + + return match.group() + + def check_operands(func): """Check the existence of the operands. @@ -41,9 +88,8 @@ def operands_wrapper(operator_symbol, operands): return func(operator_symbol, operands) elif not left_operand and right_operand: - if any([operator_symbol == "+", operator_symbol == "-"]): - return float(f"{operator_symbol}{right_operand}") - + # if any([operator_symbol == "+", operator_symbol == "-"]): + # return float(f"{operator_symbol}{right_operand}") return func(operator_symbol, [right_operand]) elif not right_operand and left_operand: @@ -52,172 +98,121 @@ def operands_wrapper(operator_symbol, operands): return operands_wrapper -def get_inner_parentheses(expression: str) -> str: - """Return the most inner parentheses in an expression.""" - pattern = re.compile(r"\([^()]*\)") - without_outer_parentheses = [inner_expr[1:-1] for inner_expr in - pattern.findall(expression)] - - if without_outer_parentheses: - return without_outer_parentheses[-1] - - return expression - - @check_operands -def calculate_sub_expression(operator_symbol, operands): +def calculate_sub_expression(symbol: str, operands: list) -> float: """Evaluate an expression. Args: operands (list): the operands, can be 2 or 1 according to operation. - operator_symbol (str): a symbol represents the mathematical operation. + symbol (str): a symbol represents the mathematical operation. Returns: A float, the result of the mathematical operation. + + Note: + Brackets need a string operand (_evaluate() takes a string) and the + others a float (factorial and negate). """ if len(operands) == 2: - return operators[operator_symbol].operation(float(operands[0]), - float(operands[1])) - - return operators[operator_symbol].operation(float(operands[0])) - + return operators[symbol].operation(float(operands[0]), + float(operands[1])) -def get_highest_precedence_operator(expression: str) -> str: - """Return the highest priority operator in a given expression.""" - without_operands = filter_operands_out(expression) - return sorted(without_operands, key=lambda ch: operators[ch].precedence, - reverse=True)[0] + operand = operands[0] + operation_to_do = operators[symbol].operation + if operation_to_do is not _evaluate: + operand = float(operands[0]) -def filter_operands_out(sub_expression: str) -> list: - """Remove operands from sub expression, leaves only operators.""" - return [ch for ch in sub_expression if is_operator(ch)] + return operation_to_do(operand) -def parse_sub_expression_by_operator(sub_expression, operator): +def parse_sub_expression(symbol: str, sub_expr: str) -> list: """Analyze a sub expression to its operands, by its operator. Args: - sub_expression (str): the expression to analyze. - operator (str): the operator of the sub expression. + symbol: the operator of the sub expression. + sub_expr: the expression to analyze. Returns: - A tuple of both operands, left and right. + A list of both operands, left and right. """ - any_int_or_float = r"-?\d+(\.\d+)?" + parsed_expr = sub_expr.split(symbol) - pattern = re.compile(rf"(?P{any_int_or_float})?\{operator}(" - rf"?P{any_int_or_float})?") + if symbol == "(": + parsed_expr[1] = parsed_expr[1][:-1] # removes ")" - matches = pattern.search(sub_expression).groupdict() - # replace None with "" - matches = {k: ("" if not v else v) for k, v in matches.items()} - - return matches["left_operand"], matches["right_operand"] + return parsed_expr def has_operations(expression: str) -> bool: """Return True if the expression has operations in it else, false.""" - return any([True if is_operator(ch) else False for ch in expression]) + for symbol in get_operators_by_precedence(): + if operators[symbol].regex.search(expression): + return True + return False -def is_number(s: str) -> bool: - """Return true if a string can be a number else, false.""" - try: - num = float(s) - except ValueError: - return False +def _evaluate(expression: str) -> float: + """Calculate the value of a mathematical expression.""" + for operator_symbol in get_operators_by_precedence(): + if not has_operations(expression): + return float(expression) - return True + sub_expression = get_sub_expression_by_symbol(operator_symbol, + expression) + if not sub_expression: + continue + operands = parse_sub_expression(operator_symbol, sub_expression) + result = calculate_sub_expression(operator_symbol, operands) -def parse_sub_expression(sub_expr: str) -> tuple: - """Parse an expression according to its highest priority operator. + new_expression = expression.replace(sub_expression, str(result)) + return _evaluate(new_expression) - Args: - sub_expr: the part of the expression to parsed. - Returns: - The left operand, the operator and the right operand. +def evaluate(expression: str) -> float: + """Evaluates a mathematical equation. Note: - The left operand or right operand can be "" if the operator is unary. - """ - operator = get_highest_precedence_operator(sub_expr) - - left_operand, right_operand = parse_sub_expression_by_operator( - sub_expr, operator) - - return left_operand, right_operand, operator - - -def replace_in_expression(old_expr, old_sub_expr, new_sub_expr): - """Replace sub expression in the entire expression. - - Args: - old_expr (str): the entire expression. - old_sub_expr (str): the expression to be replaced. - new_sub_expr (Str): the new expression to be put in. - - Returns: - str: the new entire expression. + This is a wrapper function used to remove spaces from the given + expression. """ - if is_number(new_sub_expr): - return old_expr.replace(f"({old_sub_expr})", new_sub_expr) - - return old_expr.replace(f"{old_sub_expr}", new_sub_expr) - - -def evaluate(expression: str) -> str: - """Calculates the value of a mathematical expression.""" - if is_number(expression): - return expression - - sub_expression = get_inner_parentheses(expression) - - left_operand, right_operand, current_operator = parse_sub_expression( - sub_expression) + return _evaluate(remove_spaces(expression)) - current_result = calculate_sub_expression(current_operator, - [left_operand, right_operand]) - updated_sub_expression = sub_expression.replace( - f"{left_operand}{current_operator}{right_operand}", - str(current_result)) +# dictionary containing the information about supported operations. +operators = { + '+': Operator(operator.add, 1, + fr"{any_number_regex}\+{any_number_regex}"), + '-': Operator(operator.sub, 1, + fr"{any_number_regex}-{any_number_regex}"), - expression = replace_in_expression(expression, sub_expression, - updated_sub_expression) + '*': Operator(operator.mul, 2, + fr"{any_number_regex}\*{any_number_regex}"), + '/': Operator(operator.truediv, 2, + fr"{any_number_regex}/{any_number_regex}"), - return evaluate(expression) + '^': Operator(math.pow, 3, + fr"{any_number_regex}\^{any_number_regex}"), + '%': Operator(math.fmod, 4, + fr"{any_number_regex}%{any_number_regex}"), + '@': Operator(average, 5, + fr"{any_number_regex}@{any_number_regex}"), + '$': Operator(max, 5, + fr"{any_number_regex}\${any_number_regex}"), + '&': Operator(min, 5, + fr"{any_number_regex}&{any_number_regex}"), -class Calculator: - """A class which represents a single calculator. - - Args: - expression (str): a mathematical expression to evaluate. - This argument can be with separating spaces. - - Attributes: - expression (str): the mathematical expression to evaluate. - Outer parentheses are added in setter. - - Note: - After the creation of an Calculator instance there is no need to - create a new one to calculate another expression. Just modify the - expression property to the new expression. - """ - def __init__(self, expression=""): - self.expression = expression + '~': Operator(operator.neg, 6, fr"~{any_number_regex}"), + '!': Operator(math.factorial, 7, fr"{any_number_regex}!"), - @property - def expression(self): - return self._expression + '(': Operator(_evaluate, maxsize, r"\([^()]*\)") +} - @expression.setter - def expression(self, expression): - self._expression = f"({remove_spaces(expression)})" - def calculate(self): - return float(evaluate(self._expression)) +def get_operators_by_precedence() -> list: + """Return all supported operators by their priority.""" + return sorted(operators, key=lambda ch: operators[ch].precedence, + reverse=True) diff --git a/calculator/operators.py b/calculator/operators.py deleted file mode 100644 index 19c75ef..0000000 --- a/calculator/operators.py +++ /dev/null @@ -1,53 +0,0 @@ -"""The operators supported by the calculator.""" - -import math -import operator - - -class Operator: - """Representation of a single mathematical operator. - - Attributes: - operation (function): the function that does the operation. - precedence (int): the priority of the operation. - - Note: - Precedence determines which operation goes after another. - """ - - def __init__(self, operation, precedence): - self.operation = operation - self.precedence = precedence - - -def average(num1, num2): - """Return the average of two numbers (int/float).""" - return float(num1 + num2) / 2.0 - - -"""A dictionary containing the information about supported operations.""" -operators = { - '+': Operator(operator.add, 1), - '-': Operator(operator.sub, 1), - - '*': Operator(operator.mul, 2), - '/': Operator(operator.truediv, 2), - - '^': Operator(math.pow, 3), - '%': Operator(math.fmod, 4), - - '@': Operator(average, 5), - '$': Operator(max, 5), - '&': Operator(min, 5), - - '~': Operator(operator.neg, 6), - '!': Operator(math.factorial, 7), - - '(': Operator(None, 8), - ')': Operator(None, 8) -} - - -def is_operator(symbol: str) -> bool: - """Return true if operation is supported else false.""" - return symbol in operators diff --git a/calculator/test_unittest_calculator.py b/calculator/test_unittest_calculator.py index a027cd7..db27826 100644 --- a/calculator/test_unittest_calculator.py +++ b/calculator/test_unittest_calculator.py @@ -2,59 +2,38 @@ import unittest -from calculator import Calculator +from calculator import evaluate class TestOperations(unittest.TestCase): """Test the supported operations by the calculator.""" - def __init__(self, *args, **kwargs): - super(TestOperations, self).__init__(*args, **kwargs) - self.c = Calculator() - def test_basic(self): """Test the basic operations.""" - self.c.expression = "1 + 2 + 3" - self.assertEqual(self.c.calculate(), 6.0) - - self.c.expression = "8 - 4 - 6" - self.assertEqual(self.c.calculate(), -2.0) + self.assertEqual(evaluate("1 + (2 + 3)"), 6.0) + self.assertEqual(evaluate("(8 - 4) - 6"), -2.0) - self.c.expression = "3* -5" - self.assertEqual(self.c.calculate(), -15.0) - - self.c.expression = "65 /5" - self.assertEqual(self.c.calculate(), 13.0) + self.assertEqual(evaluate("3* -5"), -15.0) + self.assertEqual(evaluate("65 /5"), 13.0) def test_advanced(self): """Test the advanced operations.""" - self.c.expression = "9 ^ 0.5" - self.assertEqual(self.c.calculate(), 3.0) - - self.c.expression = "3 % 2" - self.assertEqual(self.c.calculate(), 1.0) + self.assertEqual(evaluate("9 ^ 0.5"), 3.0) + self.assertEqual(evaluate("3 % 2"), 1.0) - self.c.expression = "15 @ 60" - self.assertEqual(self.c.calculate(), 37.5) + self.assertEqual(evaluate("15 @ 60"), 37.5) + self.assertEqual(evaluate("13 $ 16"), 16.0) + self.assertEqual(evaluate("13 & 16"), 13.0) - self.c.expression = "13 $ 16" - self.assertEqual(self.c.calculate(), 16.0) - - self.c.expression = "13 & 16" - self.assertEqual(self.c.calculate(), 13.0) - - self.c.expression = "~9" - self.assertEqual(self.c.calculate(), -9.0) - - self.c.expression = "5 !" - self.assertEqual(self.c.calculate(), 120.0) + self.assertEqual(evaluate("~9"), -9.0) + self.assertEqual(evaluate("5 !"), 120.0) def test_precedence(self): """Check the order which operations are done.""" - self.c.expression = "1 + 2 * 3 + 4 / 0.5 $ (1 / 3)" - self.assertEqual(self.c.calculate(), 15.0) - - self.c.expression = "((1 - 4)^3)*-1" - self.assertEqual(self.c.calculate(), 27.0) + self.assertEqual(evaluate("((1 - 4)^3)*-1"), 27.0) + self.assertEqual(evaluate("((1 + 2) * 9 + 4! + ~-2)"), 53.0) + self.assertEqual(evaluate("1 + 2 * 3 + 4 / 0.5 $ (1 / 3)"), 15.0) + self.assertEqual(evaluate("(0.3 + (4@0.5))$(~0.25 + ((3^-2)@0.5))"), + 2.55) if __name__ == '__main__': From 403fcb1709e3d22e92e39c0e4037170f702c9eb9 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Wed, 22 Apr 2020 21:24:52 +0300 Subject: [PATCH 09/15] Simplification and edge cases --- calculator/calculator.py | 258 ++++++++----------------- calculator/test_calculator.py | 60 ++++++ calculator/test_unittest_calculator.py | 40 ---- 3 files changed, 136 insertions(+), 222 deletions(-) create mode 100644 calculator/test_calculator.py delete mode 100644 calculator/test_unittest_calculator.py diff --git a/calculator/calculator.py b/calculator/calculator.py index 13be723..11b0229 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -1,218 +1,112 @@ """Calculator module. This module allows evaluation of strings which represent mathematical -expressions. The supported operations are declared in operators dictionary. - -Example: - To use this module, import the evaluate() function: - - from calculator import evaluate - - The parameter of this function is a mathematical expression (str). For - example, "1 + (3^-2) * (4! - 5$ 2)". +expressions. The supported operators are declared in operators dictionary. """ - -import re import math import operator +import re +from collections import namedtuple from sys import maxsize -any_number_regex = r"-?\d+(\.\d+)?" - - -class Operator: - """Representation of a single mathematical operator. - - Attributes: - operation (function): the function that does the operation. - precedence (int): the priority of the operation. - regex (str): regex to find a pattern which suits to the operator. - - Note: - Precedence determines which operation goes after another. - """ - - def __init__(self, operation, precedence, regex): - self.operation = operation - self.precedence = precedence - self.regex = re.compile(regex) - - -def average(num1, num2): - """Return the average of two numbers (int/float). - - Note: - Same as using mean() from statistics module. - """ - return float(num1 + num2) / 2.0 - - -def remove_spaces(expression: str) -> str: - """Remove the spaces between characters in a string.""" - return ''.join(expression.split()) - - -def get_sub_expression_by_symbol(symbol: str, expression: str) -> str: - """Find the sub expression the current operator. - - Args: - symbol: the current operator symbol. - expression: the entire expression to search. - - Returns: - The desired sub expression of the operator. If nothing is found - returns an empty string. - """ - match = operators[symbol].regex.search(expression) - - if not match: - return "" - - return match.group() - - -def check_operands(func): - """Check the existence of the operands. - - Args: - func (function): the function to operate on. - - Returns: - function: the function wrapper. - """ - - def operands_wrapper(operator_symbol, operands): - left_operand, right_operand = operands - - if left_operand and right_operand: - return func(operator_symbol, operands) - - elif not left_operand and right_operand: - # if any([operator_symbol == "+", operator_symbol == "-"]): - # return float(f"{operator_symbol}{right_operand}") - return func(operator_symbol, [right_operand]) - - elif not right_operand and left_operand: - return func(operator_symbol, [left_operand]) - - return operands_wrapper - +ANY_NUMBER = r"-?\d+(?:\.\d+)?" +USER_MSG = "Enter expression to evaluate (enter 'quit' to exit) >>> " + +# higher precedence means the operator stronger (will be done first) +Operator = namedtuple("Operator", ["operation", "precedence", "regex"]) + +OPERATORS = { + '+': Operator(precedence=1, operation=operator.add, + regex=fr"({ANY_NUMBER})\+({ANY_NUMBER})"), + '-': Operator(precedence=1, operation=operator.sub, + regex=fr"({ANY_NUMBER})-({ANY_NUMBER})"), + + '*': Operator(precedence=2, operation=operator.mul, + regex=fr"({ANY_NUMBER})\*({ANY_NUMBER})"), + '/': Operator(precedence=2, operation=operator.truediv, + regex=fr"({ANY_NUMBER})/({ANY_NUMBER})"), + + '^': Operator(precedence=3, operation=math.pow, + regex=fr"({ANY_NUMBER})\^({ANY_NUMBER})"), + '%': Operator(precedence=4, operation=math.fmod, + regex=fr"({ANY_NUMBER})%({ANY_NUMBER})"), + + '@': Operator(precedence=5, operation=lambda x, y: (x + y) / 2.0, + regex=fr"({ANY_NUMBER})@({ANY_NUMBER})"), + '$': Operator(precedence=5, operation=max, + regex=fr"({ANY_NUMBER})\$({ANY_NUMBER})"), + '&': Operator(precedence=5, operation=min, + regex=fr"({ANY_NUMBER})&({ANY_NUMBER})"), + + '~': Operator(precedence=6, operation=operator.neg, + regex=fr"~({ANY_NUMBER})"), + '!': Operator(precedence=7, operation=math.factorial, + regex=fr"({ANY_NUMBER})!"), + + '(': Operator(precedence=maxsize, operation=lambda expr: _evaluate(expr), + regex=r"\(([^()]*)\)") +} -@check_operands -def calculate_sub_expression(symbol: str, operands: list) -> float: - """Evaluate an expression. - Args: - operands (list): the operands, can be 2 or 1 according to operation. - symbol (str): a symbol represents the mathematical operation. +def operators_info_by_precedence() -> list: + """Sort the value of each item in OPERATORS by its priority. Returns: - A float, the result of the mathematical operation. - - Note: - Brackets need a string operand (_evaluate() takes a string) and the - others a float (factorial and negate). + List of Operator named tuples by priority, strong to weak. """ - if len(operands) == 2: - return operators[symbol].operation(float(operands[0]), - float(operands[1])) - - operand = operands[0] - operation_to_do = operators[symbol].operation - - if operation_to_do is not _evaluate: - operand = float(operands[0]) - - return operation_to_do(operand) + return sorted(OPERATORS.values(), key=operator.attrgetter("precedence"), + reverse=True) -def parse_sub_expression(symbol: str, sub_expr: str) -> list: - """Analyze a sub expression to its operands, by its operator. +def replace_in_expression(operator_regex: str, expression: str, + new_expr: str) -> str: + """Change the old sub expression to a new one according to pattern. Args: - symbol: the operator of the sub expression. - sub_expr: the expression to analyze. + operator_regex: pattern to find a sub expression of the operator. + expression: the entire expression. + new_expr: the expression to put instead of the old one. Returns: - A list of both operands, left and right. + A new whole expression, changed according to pattern. """ - parsed_expr = sub_expr.split(symbol) - - if symbol == "(": - parsed_expr[1] = parsed_expr[1][:-1] # removes ")" - - return parsed_expr - - -def has_operations(expression: str) -> bool: - """Return True if the expression has operations in it else, false.""" - for symbol in get_operators_by_precedence(): - if operators[symbol].regex.search(expression): - return True - - return False + old_expression = re.search(operator_regex, expression).group() + return expression.replace(old_expression, new_expr) def _evaluate(expression: str) -> float: """Calculate the value of a mathematical expression.""" - for operator_symbol in get_operators_by_precedence(): - if not has_operations(expression): - return float(expression) + expression = str(re.sub(r"\s+", "", expression)) + operators_by_precedence = operators_info_by_precedence() - sub_expression = get_sub_expression_by_symbol(operator_symbol, - expression) - if not sub_expression: - continue + for operator_info in operators_by_precedence: + operator_regex = operator_info.regex - operands = parse_sub_expression(operator_symbol, sub_expression) - result = calculate_sub_expression(operator_symbol, operands) + try: + operands = list(re.search(operator_regex, expression).groups()) - new_expression = expression.replace(sub_expression, str(result)) - return _evaluate(new_expression) - - -def evaluate(expression: str) -> float: - """Evaluates a mathematical equation. - - Note: - This is a wrapper function used to remove spaces from the given - expression. - """ - return _evaluate(remove_spaces(expression)) + except AttributeError: + continue + if operator_info.precedence != maxsize: + operands = list(map(float, operands)) -# dictionary containing the information about supported operations. -operators = { - '+': Operator(operator.add, 1, - fr"{any_number_regex}\+{any_number_regex}"), - '-': Operator(operator.sub, 1, - fr"{any_number_regex}-{any_number_regex}"), + result = operator_info.operation(*operands) - '*': Operator(operator.mul, 2, - fr"{any_number_regex}\*{any_number_regex}"), - '/': Operator(operator.truediv, 2, - fr"{any_number_regex}/{any_number_regex}"), + expression = replace_in_expression( + operator_regex, expression, str(result)) - '^': Operator(math.pow, 3, - fr"{any_number_regex}\^{any_number_regex}"), - '%': Operator(math.fmod, 4, - fr"{any_number_regex}%{any_number_regex}"), + return _evaluate(expression) + return float(expression) - '@': Operator(average, 5, - fr"{any_number_regex}@{any_number_regex}"), - '$': Operator(max, 5, - fr"{any_number_regex}\${any_number_regex}"), - '&': Operator(min, 5, - fr"{any_number_regex}&{any_number_regex}"), - '~': Operator(operator.neg, 6, fr"~{any_number_regex}"), - '!': Operator(math.factorial, 7, fr"{any_number_regex}!"), +def main(): + raw_expression = input(USER_MSG) - '(': Operator(_evaluate, maxsize, r"\([^()]*\)") -} + while raw_expression.lower() != "quit": + print(_evaluate(raw_expression)) + raw_expression = input(USER_MSG) -def get_operators_by_precedence() -> list: - """Return all supported operators by their priority.""" - return sorted(operators, key=lambda ch: operators[ch].precedence, - reverse=True) +if __name__ == '__main__': + main() diff --git a/calculator/test_calculator.py b/calculator/test_calculator.py new file mode 100644 index 0000000..d61c9c7 --- /dev/null +++ b/calculator/test_calculator.py @@ -0,0 +1,60 @@ +"""Unit test module for testing calculator module.""" + +import unittest + +from calculator import _evaluate + + +class TestOperations(unittest.TestCase): + """Test the supported operations by the calculator.""" + def test_simple_equations(self): + """Test basic expressions.""" + self.assertEqual(_evaluate("1 + (2 + 3)"), 6.0) + self.assertEqual(_evaluate("(8 - 4) - 6"), -2.0) + + self.assertEqual(_evaluate("3* -5"), -15.0) + self.assertEqual(_evaluate("65 /5"), 13.0) + + self.assertEqual(_evaluate("9 ^ 0.5"), 3.0) + self.assertEqual(_evaluate("3 % 2"), 1.0) + + self.assertEqual(_evaluate("15 @ 60"), 37.5) + self.assertEqual(_evaluate("13 $ 16"), 16.0) + self.assertEqual(_evaluate("13 & 16"), 13.0) + + self.assertEqual(_evaluate("~9"), -9.0) + self.assertEqual(_evaluate("5 !"), 120.0) + + def test_precedence(self): + """Test the order which operations are done.""" + self.assertEqual(_evaluate("((1 - 4)^3)*-1"), 27.0) + self.assertEqual(_evaluate("((1 + 2) * 9 + 4! + ~-2)"), 53.0) + self.assertEqual(_evaluate("1 + 2 * 3 + 4 / 0.5 $ (1 / 3)"), 15.0) + self.assertEqual(_evaluate("(0.3 + (4@0.5))$(~0.25 + ((3^-2)@0.5))"), + 2.55) + + +class TestEdgeCases(unittest.TestCase): + """Test rare cases of inputs to see function behavior""" + def test_wrong_input(self): + # un-supported operator + with self.assertRaises(ValueError): + _evaluate("1#1") + + # should be "55*0.2" + with self.assertRaises(ValueError): + _evaluate("55*.2") + + # should be "~-1" + with self.assertRaises(ValueError): + _evaluate("--1") + + with self.assertRaises(ValueError): + _evaluate("(((8@5.67) + 1)") + + with self.assertRaises(ValueError): + _evaluate('1+a*76') + + +if __name__ == '__main__': + unittest.main() diff --git a/calculator/test_unittest_calculator.py b/calculator/test_unittest_calculator.py deleted file mode 100644 index db27826..0000000 --- a/calculator/test_unittest_calculator.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Unit test module for testing calculator module.""" - -import unittest - -from calculator import evaluate - - -class TestOperations(unittest.TestCase): - """Test the supported operations by the calculator.""" - def test_basic(self): - """Test the basic operations.""" - self.assertEqual(evaluate("1 + (2 + 3)"), 6.0) - self.assertEqual(evaluate("(8 - 4) - 6"), -2.0) - - self.assertEqual(evaluate("3* -5"), -15.0) - self.assertEqual(evaluate("65 /5"), 13.0) - - def test_advanced(self): - """Test the advanced operations.""" - self.assertEqual(evaluate("9 ^ 0.5"), 3.0) - self.assertEqual(evaluate("3 % 2"), 1.0) - - self.assertEqual(evaluate("15 @ 60"), 37.5) - self.assertEqual(evaluate("13 $ 16"), 16.0) - self.assertEqual(evaluate("13 & 16"), 13.0) - - self.assertEqual(evaluate("~9"), -9.0) - self.assertEqual(evaluate("5 !"), 120.0) - - def test_precedence(self): - """Check the order which operations are done.""" - self.assertEqual(evaluate("((1 - 4)^3)*-1"), 27.0) - self.assertEqual(evaluate("((1 + 2) * 9 + 4! + ~-2)"), 53.0) - self.assertEqual(evaluate("1 + 2 * 3 + 4 / 0.5 $ (1 / 3)"), 15.0) - self.assertEqual(evaluate("(0.3 + (4@0.5))$(~0.25 + ((3^-2)@0.5))"), - 2.55) - - -if __name__ == '__main__': - unittest.main() From a324e6d0e6a6cbd70b27c6637ba59886e1c8f46d Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Thu, 23 Apr 2020 16:18:50 +0300 Subject: [PATCH 10/15] Iter and inlining --- calculator/calculator.py | 70 ++++++++++++++--------------------- calculator/test_calculator.py | 44 +++++++++++----------- 2 files changed, 49 insertions(+), 65 deletions(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 11b0229..3f94c65 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -3,16 +3,15 @@ This module allows evaluation of strings which represent mathematical expressions. The supported operators are declared in operators dictionary. """ +import re import math import operator -import re -from collections import namedtuple from sys import maxsize +from collections import namedtuple ANY_NUMBER = r"-?\d+(?:\.\d+)?" -USER_MSG = "Enter expression to evaluate (enter 'quit' to exit) >>> " -# higher precedence means the operator stronger (will be done first) +# higher precedence means the operator is stronger (will be done first) Operator = namedtuple("Operator", ["operation", "precedence", "regex"]) OPERATORS = { @@ -43,69 +42,54 @@ '!': Operator(precedence=7, operation=math.factorial, regex=fr"({ANY_NUMBER})!"), - '(': Operator(precedence=maxsize, operation=lambda expr: _evaluate(expr), + '(': Operator(precedence=maxsize, operation=lambda expr: evaluate(expr), regex=r"\(([^()]*)\)") } -def operators_info_by_precedence() -> list: - """Sort the value of each item in OPERATORS by its priority. - - Returns: - List of Operator named tuples by priority, strong to weak. - """ - return sorted(OPERATORS.values(), key=operator.attrgetter("precedence"), - reverse=True) - - -def replace_in_expression(operator_regex: str, expression: str, - new_expr: str) -> str: - """Change the old sub expression to a new one according to pattern. +def evaluate(expression: str) -> float: + """Calculate the value of a mathematical expression. Args: - operator_regex: pattern to find a sub expression of the operator. - expression: the entire expression. - new_expr: the expression to put instead of the old one. + expression: the entire mathematical expression to calculate. Returns: - A new whole expression, changed according to pattern. + The calculated result of the given expression. """ - old_expression = re.search(operator_regex, expression).group() - return expression.replace(old_expression, new_expr) - - -def _evaluate(expression: str) -> float: - """Calculate the value of a mathematical expression.""" - expression = str(re.sub(r"\s+", "", expression)) - operators_by_precedence = operators_info_by_precedence() + expression = expression.replace(" ", "") + operators_by_precedence = sorted(OPERATORS.values(), + key=operator.attrgetter("precedence"), + reverse=True) for operator_info in operators_by_precedence: operator_regex = operator_info.regex - try: - operands = list(re.search(operator_regex, expression).groups()) + search_result = re.search(operator_regex, expression) - except AttributeError: + if search_result is None: continue - if operator_info.precedence != maxsize: - operands = list(map(float, operands)) + operands = search_result.groups() + + if operator_info.operation is not OPERATORS["("].operation: + operands = [float(op) for op in operands] + + operation_result = operator_info.operation(*operands) - result = operator_info.operation(*operands) + old_expression = search_result.group() + expression = expression.replace(old_expression, str(operation_result)) - expression = replace_in_expression( - operator_regex, expression, str(result)) + return evaluate(expression) - return _evaluate(expression) return float(expression) def main(): - raw_expression = input(USER_MSG) + print("Welcome! Enter expressions to evaluate ('quit' to exit) >>> ") - while raw_expression.lower() != "quit": - print(_evaluate(raw_expression)) - raw_expression = input(USER_MSG) + for expression in iter(input, "quit"): + print(evaluate(expression)) + print("Enter expression to evaluate ('quit' to exit) >>> ") if __name__ == '__main__': diff --git a/calculator/test_calculator.py b/calculator/test_calculator.py index d61c9c7..895125a 100644 --- a/calculator/test_calculator.py +++ b/calculator/test_calculator.py @@ -2,58 +2,58 @@ import unittest -from calculator import _evaluate +from calculator import evaluate class TestOperations(unittest.TestCase): """Test the supported operations by the calculator.""" def test_simple_equations(self): """Test basic expressions.""" - self.assertEqual(_evaluate("1 + (2 + 3)"), 6.0) - self.assertEqual(_evaluate("(8 - 4) - 6"), -2.0) + self.assertEqual(evaluate("1 + (2 + 3)"), 6.0) + self.assertEqual(evaluate("(8 - 4) - 6"), -2.0) - self.assertEqual(_evaluate("3* -5"), -15.0) - self.assertEqual(_evaluate("65 /5"), 13.0) + self.assertEqual(evaluate("3* -5"), -15.0) + self.assertEqual(evaluate("65 /5"), 13.0) - self.assertEqual(_evaluate("9 ^ 0.5"), 3.0) - self.assertEqual(_evaluate("3 % 2"), 1.0) + self.assertEqual(evaluate("9 ^ 0.5"), 3.0) + self.assertEqual(evaluate("3 % 2"), 1.0) - self.assertEqual(_evaluate("15 @ 60"), 37.5) - self.assertEqual(_evaluate("13 $ 16"), 16.0) - self.assertEqual(_evaluate("13 & 16"), 13.0) + self.assertEqual(evaluate("15 @ 60"), 37.5) + self.assertEqual(evaluate("13 $ 16"), 16.0) + self.assertEqual(evaluate("13 & 16"), 13.0) - self.assertEqual(_evaluate("~9"), -9.0) - self.assertEqual(_evaluate("5 !"), 120.0) + self.assertEqual(evaluate("~9"), -9.0) + self.assertEqual(evaluate("5 !"), 120.0) def test_precedence(self): """Test the order which operations are done.""" - self.assertEqual(_evaluate("((1 - 4)^3)*-1"), 27.0) - self.assertEqual(_evaluate("((1 + 2) * 9 + 4! + ~-2)"), 53.0) - self.assertEqual(_evaluate("1 + 2 * 3 + 4 / 0.5 $ (1 / 3)"), 15.0) - self.assertEqual(_evaluate("(0.3 + (4@0.5))$(~0.25 + ((3^-2)@0.5))"), + self.assertEqual(evaluate("((1 - 4)^3)*-1"), 27.0) + self.assertEqual(evaluate("((1 + 2) * 9 + 4! + ~-2)"), 53.0) + self.assertEqual(evaluate("1 + 2 * 3 + 4 / 0.5 $ (1 / 3)"), 15.0) + self.assertEqual(evaluate("(0.3 + (4@0.5))$(~0.25 + ((3^-2)@0.5))"), 2.55) class TestEdgeCases(unittest.TestCase): - """Test rare cases of inputs to see function behavior""" + """Test rare cases of inputs to see function behavior.""" def test_wrong_input(self): # un-supported operator with self.assertRaises(ValueError): - _evaluate("1#1") + evaluate("1#1") # should be "55*0.2" with self.assertRaises(ValueError): - _evaluate("55*.2") + evaluate("55*.2") # should be "~-1" with self.assertRaises(ValueError): - _evaluate("--1") + evaluate("--1") with self.assertRaises(ValueError): - _evaluate("(((8@5.67) + 1)") + evaluate("(((8@5.67) + 1)") with self.assertRaises(ValueError): - _evaluate('1+a*76') + evaluate('1+a*76') if __name__ == '__main__': From 56485ad94d2317d32b000becd87affd0a5dbacab Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Thu, 23 Apr 2020 18:00:15 +0300 Subject: [PATCH 11/15] Checking more edge cases and generic code --- calculator/calculator.py | 101 ++++++++++++++++++---------------- calculator/test_calculator.py | 2 + 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 3f94c65..1c04c9f 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -3,48 +3,53 @@ This module allows evaluation of strings which represent mathematical expressions. The supported operators are declared in operators dictionary. """ -import re import math import operator -from sys import maxsize +import re from collections import namedtuple +from sys import maxsize -ANY_NUMBER = r"-?\d+(?:\.\d+)?" +ANY_NUMBER = r"[-+]?\d+(?:\.\d+)?" # higher precedence means the operator is stronger (will be done first) Operator = namedtuple("Operator", ["operation", "precedence", "regex"]) -OPERATORS = { - '+': Operator(precedence=1, operation=operator.add, - regex=fr"({ANY_NUMBER})\+({ANY_NUMBER})"), - '-': Operator(precedence=1, operation=operator.sub, - regex=fr"({ANY_NUMBER})-({ANY_NUMBER})"), - - '*': Operator(precedence=2, operation=operator.mul, - regex=fr"({ANY_NUMBER})\*({ANY_NUMBER})"), - '/': Operator(precedence=2, operation=operator.truediv, - regex=fr"({ANY_NUMBER})/({ANY_NUMBER})"), - - '^': Operator(precedence=3, operation=math.pow, - regex=fr"({ANY_NUMBER})\^({ANY_NUMBER})"), - '%': Operator(precedence=4, operation=math.fmod, - regex=fr"({ANY_NUMBER})%({ANY_NUMBER})"), - - '@': Operator(precedence=5, operation=lambda x, y: (x + y) / 2.0, - regex=fr"({ANY_NUMBER})@({ANY_NUMBER})"), - '$': Operator(precedence=5, operation=max, - regex=fr"({ANY_NUMBER})\$({ANY_NUMBER})"), - '&': Operator(precedence=5, operation=min, - regex=fr"({ANY_NUMBER})&({ANY_NUMBER})"), - - '~': Operator(precedence=6, operation=operator.neg, - regex=fr"~({ANY_NUMBER})"), - '!': Operator(precedence=7, operation=math.factorial, - regex=fr"({ANY_NUMBER})!"), - - '(': Operator(precedence=maxsize, operation=lambda expr: evaluate(expr), - regex=r"\(([^()]*)\)") -} +OPERATORS = [ + Operator(precedence=1, operation=operator.add, + regex=fr"({ANY_NUMBER})\+({ANY_NUMBER})"), + Operator(precedence=1, operation=operator.sub, + regex=fr"({ANY_NUMBER})-({ANY_NUMBER})"), + Operator(precedence=2, operation=operator.mul, + regex=fr"({ANY_NUMBER})\*({ANY_NUMBER})"), + Operator(precedence=2, operation=operator.truediv, + regex=fr"({ANY_NUMBER})/({ANY_NUMBER})"), + Operator(precedence=3, operation=math.pow, + regex=fr"({ANY_NUMBER})\^({ANY_NUMBER})"), + Operator(precedence=4, operation=math.fmod, + regex=fr"({ANY_NUMBER})%({ANY_NUMBER})"), + Operator(precedence=5, operation=lambda x, y: (x + y) / 2.0, + regex=fr"({ANY_NUMBER})@({ANY_NUMBER})"), + Operator(precedence=5, operation=max, + regex=fr"({ANY_NUMBER})\$({ANY_NUMBER})"), + Operator(precedence=5, operation=min, + regex=fr"({ANY_NUMBER})&({ANY_NUMBER})"), + Operator(precedence=6, operation=operator.neg, + regex=fr"~({ANY_NUMBER})"), + Operator(precedence=7, operation=math.factorial, + regex=fr"({ANY_NUMBER})!"), + Operator(precedence=maxsize, operation=lambda expr: evaluate(expr), + regex=r"\(([^()]*)\)") +] + + +def is_number(obj) -> bool: + try: + float(obj) + + except TypeError: + return False + + return True def evaluate(expression: str) -> float: @@ -57,29 +62,33 @@ def evaluate(expression: str) -> float: The calculated result of the given expression. """ expression = expression.replace(" ", "") - operators_by_precedence = sorted(OPERATORS.values(), - key=operator.attrgetter("precedence"), - reverse=True) + operators_by_precedence = sorted(OPERATORS, reverse=True, + key=operator.attrgetter("precedence")) for operator_info in operators_by_precedence: operator_regex = operator_info.regex search_result = re.search(operator_regex, expression) - if search_result is None: - continue + if search_result is not None: + operands = search_result.groups() + + if all(True if re.match(f"^{ANY_NUMBER}$", op) else False for op + in operands): + operands = [float(op) for op in operands] - operands = search_result.groups() + operation_result = operator_info.operation(*operands) - if operator_info.operation is not OPERATORS["("].operation: - operands = [float(op) for op in operands] + if operation_result > 0: + new_sub_expr = f"+{operation_result}" - operation_result = operator_info.operation(*operands) + else: + new_sub_expr = str(operation_result) - old_expression = search_result.group() - expression = expression.replace(old_expression, str(operation_result)) + old_expression = search_result.group() + expression = expression.replace(old_expression, new_sub_expr) - return evaluate(expression) + return evaluate(expression) return float(expression) diff --git a/calculator/test_calculator.py b/calculator/test_calculator.py index 895125a..588ea7b 100644 --- a/calculator/test_calculator.py +++ b/calculator/test_calculator.py @@ -55,6 +55,8 @@ def test_wrong_input(self): with self.assertRaises(ValueError): evaluate('1+a*76') + self.assertEqual(evaluate("-1-2*-2"), 3.0) + if __name__ == '__main__': unittest.main() From 889772536778fc96edb73f4ec211b21542d21da4 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Thu, 23 Apr 2020 18:01:17 +0300 Subject: [PATCH 12/15] Remove unused function and imports order --- calculator/calculator.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 1c04c9f..70000ed 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -3,11 +3,11 @@ This module allows evaluation of strings which represent mathematical expressions. The supported operators are declared in operators dictionary. """ +import re import math import operator -import re -from collections import namedtuple from sys import maxsize +from collections import namedtuple ANY_NUMBER = r"[-+]?\d+(?:\.\d+)?" @@ -42,16 +42,6 @@ ] -def is_number(obj) -> bool: - try: - float(obj) - - except TypeError: - return False - - return True - - def evaluate(expression: str) -> float: """Calculate the value of a mathematical expression. From 6c5da73f7215acbc4bd1d153280d43d0ed1eff25 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Thu, 23 Apr 2020 18:09:49 +0300 Subject: [PATCH 13/15] Condition simplification --- calculator/calculator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 70000ed..83b7571 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -69,12 +69,11 @@ def evaluate(expression: str) -> float: operation_result = operator_info.operation(*operands) + new_sub_expr = str(operation_result) + if operation_result > 0: new_sub_expr = f"+{operation_result}" - else: - new_sub_expr = str(operation_result) - old_expression = search_result.group() expression = expression.replace(old_expression, new_sub_expr) From 44f85c70079c24883dce0537bab0c959db959ff3 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Thu, 23 Apr 2020 20:42:56 +0300 Subject: [PATCH 14/15] Partial and fullmatch --- calculator/calculator.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 83b7571..510e7c5 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -7,6 +7,7 @@ import math import operator from sys import maxsize +from functools import partial from collections import namedtuple ANY_NUMBER = r"[-+]?\d+(?:\.\d+)?" @@ -46,10 +47,10 @@ def evaluate(expression: str) -> float: """Calculate the value of a mathematical expression. Args: - expression: the entire mathematical expression to calculate. + expression (str): the entire mathematical expression to calculate. Returns: - The calculated result of the given expression. + float: the calculated result of the given expression. """ expression = expression.replace(" ", "") operators_by_precedence = sorted(OPERATORS, reverse=True, @@ -58,13 +59,12 @@ def evaluate(expression: str) -> float: for operator_info in operators_by_precedence: operator_regex = operator_info.regex - search_result = re.search(operator_regex, expression) + search_result = re.search(operator_regex, str(expression)) if search_result is not None: operands = search_result.groups() - if all(True if re.match(f"^{ANY_NUMBER}$", op) else False for op - in operands): + if all(re.fullmatch(ANY_NUMBER, op) for op in operands): operands = [float(op) for op in operands] operation_result = operator_info.operation(*operands) @@ -83,11 +83,10 @@ def evaluate(expression: str) -> float: def main(): - print("Welcome! Enter expressions to evaluate ('quit' to exit) >>> ") + msg_user = "Enter expression to evaluate ('quit' to exit) >>> " - for expression in iter(input, "quit"): + for expression in iter(partial(input, msg_user), "quit"): print(evaluate(expression)) - print("Enter expression to evaluate ('quit' to exit) >>> ") if __name__ == '__main__': From 2617ba6c450113919e4fc1711970746e28b1ba21 Mon Sep 17 00:00:00 2001 From: Yonatan Evantal Date: Thu, 23 Apr 2020 20:45:01 +0300 Subject: [PATCH 15/15] Convention --- calculator/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calculator/calculator.py b/calculator/calculator.py index 510e7c5..cd64d5f 100644 --- a/calculator/calculator.py +++ b/calculator/calculator.py @@ -50,7 +50,7 @@ def evaluate(expression: str) -> float: expression (str): the entire mathematical expression to calculate. Returns: - float: the calculated result of the given expression. + float. The calculated result of the given expression. """ expression = expression.replace(" ", "") operators_by_precedence = sorted(OPERATORS, reverse=True,