From 3e2ed9bb53eefd69efbad8175f2513b2f5fe5e64 Mon Sep 17 00:00:00 2001 From: Aliaksei Buziuma Date: Sat, 10 Nov 2018 18:14:21 +0300 Subject: [PATCH 01/17] update CI script and hometask Signed-off-by: Aliaksei Buziuma --- final_task/README.md | 10 +++++----- pycalc_checker.py | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/final_task/README.md b/final_task/README.md index d05c634..0de8450 100644 --- a/final_task/README.md +++ b/final_task/README.md @@ -27,14 +27,14 @@ error explanation **with "ERROR: " prefix** and exit with non-zero exit code: ```shell $ pycalc '15(25+1' ERROR: brackets are not balanced -$ pycalc 'sin(-Pi/4)**1.5' -ERROR: negative number cannot be raised to a fractional power +$ pycalc 'func' +ERROR: unknown function 'func' ``` ### Mathematical operations calculator must support * Arithmetic (`+`, `-`, `*`, `/`, `//`, `%`, `^`) (`^` is a power). * Comparison (`<`, `<=`, `==`, `!=`, `>=`, `>`). -* 3 built-in python functions: `abs`, `pow`, `round`. +* 2 built-in python functions: `abs` and `round`. * All functions and constants from standard python module `math` (trigonometry, logarithms, etc.). @@ -89,9 +89,9 @@ These requirements are not mandatory for implementation, but you can get more po ```shell $ pycalc 'sin(pi/2)' 1.0 - $ pycalc -m my_module 'sin(pi/2)' + $ pycalc 'sin(pi/2)' -m my_module 42 - $ pycalc -m time 'time()/3600/24/365' + $ pycalc 'time()/3600/24/365' -m time 48.80147332327218 ``` diff --git a/pycalc_checker.py b/pycalc_checker.py index f1525cd..4009851 100644 --- a/pycalc_checker.py +++ b/pycalc_checker.py @@ -33,6 +33,9 @@ "log10(100)": log10(100), "sin(pi/2)*111*6": sin(pi/2)*111*6, "2*sin(pi/2)": 2*sin(pi/2), + "pow(2, 3)": pow(2, 3), + "abs(-5)": abs(-5), + "round(123.4567890)": round(123.4567890), } ASSOCIATIVE = { From a53cf47ec8d8062043bf306a70455a5daeea92b2 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 18 Nov 2018 22:37:29 +0300 Subject: [PATCH 02/17] add pycalc --- final_task/pycalc/core/__init__.py | 0 final_task/pycalc/core/args.py | 17 + final_task/pycalc/core/calculator.py | 484 ++++++++++++++++++++ final_task/pycalc/core/calculator_helper.py | 76 +++ final_task/pycalc/main.py | 27 ++ final_task/pycalc/setup.py | 15 + final_task/pycalc/tests/test.py | 34 ++ 7 files changed, 653 insertions(+) create mode 100644 final_task/pycalc/core/__init__.py create mode 100644 final_task/pycalc/core/args.py create mode 100644 final_task/pycalc/core/calculator.py create mode 100644 final_task/pycalc/core/calculator_helper.py create mode 100755 final_task/pycalc/main.py create mode 100755 final_task/pycalc/setup.py create mode 100644 final_task/pycalc/tests/test.py diff --git a/final_task/pycalc/core/__init__.py b/final_task/pycalc/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/final_task/pycalc/core/args.py b/final_task/pycalc/core/args.py new file mode 100644 index 0000000..48b6d81 --- /dev/null +++ b/final_task/pycalc/core/args.py @@ -0,0 +1,17 @@ +""" +This module allows to work with command line arguments +""" +import argparse + + +def arg_parser(): + """ + Parse arguments of command line + + :return: arguments of command line + """ + parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('EXPRESSION', help='expression string to evaluate') + parser.add_argument('-m', '--use-modules', nargs='+', dest='MODULE', help='additional modules to use') + + return parser.parse_args() diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py new file mode 100644 index 0000000..7acce78 --- /dev/null +++ b/final_task/pycalc/core/calculator.py @@ -0,0 +1,484 @@ +""" +This module allows to calculator functionality +""" + +import math +import builtins +import os +import re +import sys +import operator as op +from core.calculator_helper import PycalcError +from core.calculator_helper import ( + is_number, may_unary_operator, is_unary_operator, check_right_associativity, is_callable, + check_empty_expression, check_brackets_balance +) + +PRIORITIES = { + 1: ['<', '>', '<=', '>=', '==', '!='], + 2: ['+', '-'], + 3: ['*', '/', '//', '%'], + 4: ['u+', 'u-'], + 5: ['^'] +} + +MODULES = [builtins, math] + +BUILTINS_FUNCS = ['abs', 'round'] + +OPERATORS = { + '+': op.add, + '-': op.sub, + 'u-': -1, + 'u+': 1, + '*': op.mul, + '/': op.truediv, + '//': op.floordiv, + '%': op.mod, + '^': op.pow, + '<': op.lt, + '>': op.gt, + '<=': op.le, + '>=': op.ge, + '!=': op.ne, + '==': op.eq +} + + +def import_modules(modules): + """ + Import modules + """ + sys.path.insert(0, os.path.abspath(os.path.curdir)) + for module in modules: + try: + import_module = __import__(module) + MODULES.insert(0, import_module) + except ModuleNotFoundError: + raise PycalcError(f'Module "{module}" not found') + + +def split_operands(merged_operand): + """ + Split operands + :param merged_operand + :return: list of operands + """ + operands = [] + position = len(merged_operand) + while merged_operand: + if position < 0: + raise PycalcError('Unexpected operand') + current_string = merged_operand[:position] + if get_module(current_string) or is_number(current_string): + operands.append(current_string) + merged_operand = merged_operand[position:] + position = len(merged_operand) + else: + position -= 1 + return operands + + +def implicit_multiplication(expression): + """ + Finds places where multiplication signs are missing and inserts them there + :param expression + :return: expression with multiply signs where they are missing + """ + operator_end_position = -1 + insert_positions = [] + expression = expression.replace(' ', '') + expression = re.sub(r'\)\(', ')*(', expression) + for i in range(len(expression)): + if operator_end_position >= i: + continue + j = i + operand, operator_end_position, i = find_operand(expression, i) + if operand: + operands = split_operands(operand) + if not operands: + raise PycalcError('Unexpected operand') + elif len(operands) > 1: + i = j + splited_operands = '*'.join(operands) + if i > 0 and expression[i - 1] == ')': + splited_operands = '*' + splited_operands + expression = expression.replace(operand, splited_operands) + operator_end_position = i + len('*'.join(operands[:-1])) + operand = operands[-1] + module = get_module(operand) + is_call = False + if module: + is_call = is_callable(operand, module) + if operand and is_number(operand) or module: + j = i - len(operand) - 1 + if j > 0: + if j and j != i and expression[j] == ')': + insert_positions.append(j + 1 + len(insert_positions)) + j = i + if j < len(expression) and expression[j] == '(' and not is_call: + insert_positions.append(j + len(insert_positions)) + expression = list(expression) + for i in insert_positions: + expression.insert(i, '*') + return ''.join(expression) + + +def may_valid_operation(operator, cur): + """ + :param operator: + :param cur: + :return: + """ + if is_unary_operator(cur) and get_priority(cur) < get_priority(operator): + return + return True + + +def get_module(attribute): + """ + :return: module that contains the attribute + """ + for module in MODULES: + if hasattr(module, attribute): + if module == builtins and attribute not in BUILTINS_FUNCS: + continue + return module + return + + +def split_arguments(arguments_string): + """ + Get function arguments from string + :param arguments_string + :return: arguments + """ + brackets = {'(': 1, ')': -1} + count = 0 + split_positions = [] + arguments = [] + for i, symbol in enumerate(arguments_string): + if symbol in brackets: + count += brackets[symbol] + elif symbol == ',' and not count: + split_positions.append(i) + + for i, position in enumerate(split_positions): + if not i: + arguments.append(arguments_string[:position]) + elif i < len(split_positions): + arguments.append(arguments_string[split_positions[i - 1] + 1:position]) + + if split_positions: + arguments.append(arguments_string[split_positions[-1] + 1:]) + elif not arguments: + arguments.append(arguments_string) + return arguments + + +def process_func_or_const(operand, expression, operator_end_position, module): + """ + :param operand + :param expression + :param operator_end_position + :param module + :return: the constant or result of the function and + position of the last symbol if this is function + """ + if callable(getattr(module, operand)): + count = 0 + inner_expression = '' + brackets = {'(': 1, ')': -1} + if operator_end_position < len(expression) and expression[operator_end_position] == '(': + count += 1 + operator_end_position += 1 + while count: + if expression[operator_end_position] in brackets: + count += brackets[expression[operator_end_position]] + if not count: + break + inner_expression += expression[operator_end_position] + operator_end_position += 1 + args = [] + if inner_expression: + raw_arguments = split_arguments(inner_expression) + if not raw_arguments[-1]: + raw_arguments.pop() + args = [calculate(arg) for arg in raw_arguments] + try: + func_result = getattr(module, operand)(*args) + if not is_number(func_result): + raise PycalcError('Unsupported function result') + return func_result, operator_end_position + except (TypeError, ValueError) as error: + raise PycalcError(error) + + return getattr(module, operand), '' + + +def get_priority(operator): + """ + :param operator + :return: priority of operator + """ + for priority, operators in PRIORITIES.items(): + if operator in operators: + return priority + return -1 + + +def check_comparison_priority(operators): + """ + :param operators + :return: True if the list of operators is only comparison operators + False otherwise + """ + for operator in operators: + if get_priority(operator) > 1: + return + return True + + +def check_valid_spaces(expression): + """ + Check spaces for validity + :param expression + """ + is_last_number, is_last_operator, is_space = False, False, False + operator_end_position = -1 + for i, symbol in enumerate(expression): + if symbol in ['(', ')']: + is_last_number, is_last_operator, is_space = False, False, False + elif operator_end_position >= i: + continue + elif symbol == ' ': + is_space = True + elif symbol in OPERATORS or (i < len(expression) - 1 and symbol + expression[i + 1] in OPERATORS): + if is_last_operator and is_space and not may_unary_operator(symbol): + raise PycalcError('Missing operand') + else: + operator_end_position = i + get_length_operator(expression, i) - 1 + is_last_number, is_last_operator, is_space = False, True, False + else: + if symbol in ['!', '=']: + raise PycalcError('Invalid operator') + is_last_operator, is_space = False, False + operand, operator_end_position, i = find_operand(expression, i) + if is_number(operand): + if is_last_number: + raise PycalcError('Missing operator') + is_last_number = True + else: + is_last_number = False + + +def execute_comparison(operands, operators): + """ + :param operands + :param operators + :return: False if at least one comparison return False + True otherwise + """ + if len(operands) == len(operators) + 1: + for i, operator in enumerate(operators): + if not OPERATORS[operator](operands[i], operands[i + 1]): + return False + return True + raise PycalcError('Missing operator or operand') + + +def execute_operation(operands, operator): + """ + Execute operation with operands and put result into list of the operands + :param operands + :param operator + """ + if operands: + right = operands.pop() + if operator.startswith('u'): + operands.append(OPERATORS[operator] * right) + elif operands: + left = operands.pop() + operands.append(OPERATORS[operator](left, right)) + else: + raise PycalcError('Missing operator or operand') + + +def final_execution(operators, operands): + """ + Execute operations with operands while list of operators not empty + :param operators + :param operands + :return: result of calculation + """ + if not operands: + raise PycalcError('Missing operator') + while operators: + if not check_comparison_priority(operators): + execute_operation(operands, operators.pop()) + else: + operands = [execute_comparison(operands, operators)] + break + + if isinstance(operands[-1], complex): + raise PycalcError('Negative number cannot be raised to a fractional power') + + elif len(operands) > 1: + raise PycalcError('Missing operator') + + answer = operands.pop() + if answer % 1 or isinstance(answer, bool): + return answer + return int(answer) + + +def find_operand(expression, position): + """ + :param expression + :param position + :return: operand, position of the last symbol of the operand, + position of symbol after operand + """ + operand = '' + while position < len(expression) and (expression[position].isalnum() or expression[position] == '.'): + operand += expression[position] + position += 1 + operator_end_position = position - 1 + + return operand, operator_end_position, position + + +def get_length_operator(expression, position): + """ + :param expression + :param position + :return: length of operator + """ + if position < len(expression) - 1 and expression[position] + expression[position + 1] in OPERATORS: + return 2 + return 1 + + +def validate_expression(expression): + """ + Validate expression + :param expression + """ + check_empty_expression(expression) + check_brackets_balance(expression) + check_valid_spaces(expression) + + +def update_operands(expression, operands, i): + """ + :param expression: + :param operands: + :param i: + :return: + """ + operand, operator_end_position, i = find_operand(expression, i) + + module = get_module(operand) + if is_number(operand): + operand = float(operand) + if operand % 1: + operands.append(operand) + else: + operands.append(int(operand)) + elif module: + new_operand = process_func_or_const(operand, expression, i, module) + operands.append(new_operand[0]) + if new_operand[1]: + operator_end_position = new_operand[1] + else: + raise PycalcError('Unexpected operand') + is_unary = False + + return operator_end_position, is_unary + + +def update_operators(expression, operator, operators, operands, is_unary, operator_end_position, i): + """ + :param expression: + :param operator: + :param operators: + :param operands: + :param is_unary: + :param operator_end_position: + :param i: + :return: + """ + if i: + prev_symbol = expression[i - 1] + if prev_symbol in OPERATORS: + is_unary = True + + if i < len(expression) - 1 and operator + expression[i + 1] in OPERATORS: + operator += expression[i + 1] + operator_end_position = i + 1 + + if is_unary and may_unary_operator(operator): + operator = 'u' + operator + + if get_priority(operator) >= 1 or (is_unary and may_unary_operator(operator)): + while operators and operands: + condition_a = not check_right_associativity(operator) + condition_b = get_priority(operators[-1]) >= get_priority(operator) + condition_c = check_right_associativity(operator) + condition_d = get_priority(operators[-1]) > get_priority(operator) + condition = condition_a and condition_b or condition_c and condition_d + + if may_valid_operation(operators[-1], operator) and get_priority(operators[-1]) > 1 and condition: + execute_operation(operands, operators.pop()) + else: + break + operators.append(operator) + return operator_end_position + + +def calculate(expression): + """ + :param expression + :return: result of calculation + """ + check_empty_expression(expression) + operands = [] + operators = [] + operator_end_position = -1 + is_unary = True + for i, symbol in enumerate(expression): + if operator_end_position >= i: + continue + elif symbol == '(': + operators.append(symbol) + is_unary = True + elif symbol == ')': + while operators[-1] != '(': + execute_operation(operands, operators.pop()) + operators.pop() + is_unary = False + elif symbol in OPERATORS or (i < len(expression) - 1 and symbol + expression[i + 1] in OPERATORS): + operator_end_position = update_operators(expression, symbol, operators, operands, + is_unary, operator_end_position, i) + else: + operator_end_position, is_unary = update_operands(expression, operands, i) + + return final_execution(operators, operands) + + +def calculator(expression, modules=None): + """ + :param expression + :param modules + :return: result of calculation + """ + try: + if modules: + import_modules(modules) + + validate_expression(expression) + expression = implicit_multiplication(expression) + return calculate(expression) + + except PycalcError as error: + return error diff --git a/final_task/pycalc/core/calculator_helper.py b/final_task/pycalc/core/calculator_helper.py new file mode 100644 index 0000000..84a77a2 --- /dev/null +++ b/final_task/pycalc/core/calculator_helper.py @@ -0,0 +1,76 @@ +""" +This module contains auxiliary functions for the calculator +""" + + +class PycalcError(ArithmeticError): + def __init__(self, message): + super().__init__('ERROR: ' + str(message)) + + +def is_number(string): + """ + :return: True if the string is a number + False otherwise + """ + try: + float(string) + return True + except ValueError: + return + + +def may_unary_operator(operator): + """ + :return: True if the operator may be unary + False otherwise + """ + return operator in ['+', '-'] + + +def is_unary_operator(operator): + """ + :return: True if the operator is unary + False otherwise + """ + return operator in ['u+', 'u-'] + + +def check_right_associativity(operator): + """ + :return: True if the operator is ^ + False otherwise + """ + return operator == '^' + + +def is_callable(attribute, module): + """ + :return: True if the attribute is callable + False otherwise + """ + return callable(getattr(module, attribute)) + + +def check_brackets_balance(expression): + """ + :return: True if the brackets are balanced + False otherwise + """ + brackets = {'(': 1, ')': -1} + count = 0 + for symbol in expression: + if symbol in brackets: + count += brackets[symbol] + if count < 0: + raise PycalcError('Brackets are not balanced') + if count: + raise PycalcError('Brackets are not balanced') + + +def check_empty_expression(expression): + """ + :raise exception if expression is empty + """ + if not expression: + raise PycalcError('Empty expression while execute') diff --git a/final_task/pycalc/main.py b/final_task/pycalc/main.py new file mode 100755 index 0000000..aa3c2f2 --- /dev/null +++ b/final_task/pycalc/main.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +""" +This module allows you to work +with the functionality of calculator +""" + +import sys +from core import args +from core.calculator import calculator + + +def main(): + """ + Entry point to pycalc + + print calculated value + """ + arguments = args.arg_parser() + + answer = calculator(arguments.EXPRESSION, arguments.MODULE) + print(answer) + if str(answer).startswith('ERROR'): + sys.exit(-1) + + +if __name__ == '__main__': + main() diff --git a/final_task/pycalc/setup.py b/final_task/pycalc/setup.py new file mode 100755 index 0000000..dce50ff --- /dev/null +++ b/final_task/pycalc/setup.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +""" +This module allows to installs the application +""" +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + packages=find_packages(), + py_modules=['main'], + entry_points={ + 'console_scripts': ['pycalc = main:main'] + } +) diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py new file mode 100644 index 0000000..9e6b871 --- /dev/null +++ b/final_task/pycalc/tests/test.py @@ -0,0 +1,34 @@ +import unittest +import math +from core.calculator import calculator + + +class TestPycalc(unittest.TestCase): + + def test_implicit_multiplication(self): + self.assertEqual(calculator('5(5 + 5)', None), 50) + + def test_error(self): + self.assertTrue(calculator('5 5', None).startswith('ERROR:')) + + def test_priority(self): + self.assertEqual(calculator('2 + 2 * 2', None), 6) + + def test_functions(self): + self.assertEqual(calculator('sin(5)', None), math.sin(5)) + + def test_constants(self): + self.assertEqual(calculator('pi + e', None), math.pi + math.e) + + def test_right_associative(self): + self.assertEqual(calculator('2^4^6', None), 2**4**6) + + def test_multiple_comparison(self): + self.assertEqual(calculator('5 > 2 > 1', None), 5 > 2 > 1) + + def test_unary_operations(self): + self.assertEqual(calculator('5--5', None), 5--5) + + +if __name__ == '__main__': + unittest.main() From 6ea7324c6769cbe9e99d3e1161b054bc17fee0a7 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 18 Nov 2018 22:54:01 +0300 Subject: [PATCH 03/17] remove tests --- final_task/pycalc/tests/test.py | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index 9e6b871..512db61 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -1,34 +1,2 @@ import unittest import math -from core.calculator import calculator - - -class TestPycalc(unittest.TestCase): - - def test_implicit_multiplication(self): - self.assertEqual(calculator('5(5 + 5)', None), 50) - - def test_error(self): - self.assertTrue(calculator('5 5', None).startswith('ERROR:')) - - def test_priority(self): - self.assertEqual(calculator('2 + 2 * 2', None), 6) - - def test_functions(self): - self.assertEqual(calculator('sin(5)', None), math.sin(5)) - - def test_constants(self): - self.assertEqual(calculator('pi + e', None), math.pi + math.e) - - def test_right_associative(self): - self.assertEqual(calculator('2^4^6', None), 2**4**6) - - def test_multiple_comparison(self): - self.assertEqual(calculator('5 > 2 > 1', None), 5 > 2 > 1) - - def test_unary_operations(self): - self.assertEqual(calculator('5--5', None), 5--5) - - -if __name__ == '__main__': - unittest.main() From c9ae1f87533d96f8d3352bca2bd56edfcd2ec82b Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 18 Nov 2018 23:19:28 +0300 Subject: [PATCH 04/17] fix setup --- final_task/pycalc/setup.py | 15 --------------- final_task/setup.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/final_task/pycalc/setup.py b/final_task/pycalc/setup.py index dce50ff..e69de29 100755 --- a/final_task/pycalc/setup.py +++ b/final_task/pycalc/setup.py @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -""" -This module allows to installs the application -""" -from setuptools import setup, find_packages - -setup( - name='pycalc', - version='1.0', - packages=find_packages(), - py_modules=['main'], - entry_points={ - 'console_scripts': ['pycalc = main:main'] - } -) diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..be96d50 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,14 @@ +""" +This module allows to installs the application +""" +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + packages=find_packages(), + py_modules=['main'], + entry_points={ + 'console_scripts': ['pycalc = pycalc.main:main'] + } +) From 5c49201a7dd2249d519149c1f8bf1cdad35fa23e Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 18 Nov 2018 23:26:33 +0300 Subject: [PATCH 05/17] remove setup --- final_task/pycalc/setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 final_task/pycalc/setup.py diff --git a/final_task/pycalc/setup.py b/final_task/pycalc/setup.py deleted file mode 100755 index e69de29..0000000 From 0ca1dd42f349a0d57cda57878d9b939246c742b0 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 18 Nov 2018 23:33:12 +0300 Subject: [PATCH 06/17] change version --- final_task/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/setup.py b/final_task/setup.py index be96d50..0986c49 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -5,7 +5,7 @@ setup( name='pycalc', - version='1.0', + version='1.1', packages=find_packages(), py_modules=['main'], entry_points={ From 03090b67cc1e207635c14e8b689f02ede80b556d Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 18 Nov 2018 23:49:01 +0300 Subject: [PATCH 07/17] fix imports --- final_task/pycalc/__init__.py | 0 final_task/pycalc/core/calculator.py | 4 ++-- final_task/pycalc/main.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 final_task/pycalc/__init__.py diff --git a/final_task/pycalc/__init__.py b/final_task/pycalc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 7acce78..b20d4ef 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -8,8 +8,8 @@ import re import sys import operator as op -from core.calculator_helper import PycalcError -from core.calculator_helper import ( +from .calculator_helper import PycalcError +from .calculator_helper import ( is_number, may_unary_operator, is_unary_operator, check_right_associativity, is_callable, check_empty_expression, check_brackets_balance ) diff --git a/final_task/pycalc/main.py b/final_task/pycalc/main.py index aa3c2f2..5eb6467 100755 --- a/final_task/pycalc/main.py +++ b/final_task/pycalc/main.py @@ -5,8 +5,8 @@ """ import sys -from core import args -from core.calculator import calculator +from pycalc.core import args +from pycalc.core.calculator import calculator def main(): From 3f41c81189dcbe45850bbefcc360bf425c1004e1 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sat, 24 Nov 2018 03:44:13 +0300 Subject: [PATCH 08/17] add tests & code refactoring --- final_task/pycalc/core/calculator.py | 10 +-- final_task/pycalc/core/calculator_helper.py | 2 +- final_task/pycalc/main.py | 5 +- final_task/pycalc/tests/test.py | 99 +++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 108 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index b20d4ef..9c0b818 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -124,13 +124,13 @@ def implicit_multiplication(expression): return ''.join(expression) -def may_valid_operation(operator, cur): +def may_valid_operation(last_operator, current_operator): """ - :param operator: - :param cur: - :return: + :param last_operator + :param current_operator + :return: True if """ - if is_unary_operator(cur) and get_priority(cur) < get_priority(operator): + if is_unary_operator(current_operator) and get_priority(current_operator) < get_priority(last_operator): return return True diff --git a/final_task/pycalc/core/calculator_helper.py b/final_task/pycalc/core/calculator_helper.py index 84a77a2..e44f6f1 100644 --- a/final_task/pycalc/core/calculator_helper.py +++ b/final_task/pycalc/core/calculator_helper.py @@ -3,7 +3,7 @@ """ -class PycalcError(ArithmeticError): +class PycalcError(Exception): def __init__(self, message): super().__init__('ERROR: ' + str(message)) diff --git a/final_task/pycalc/main.py b/final_task/pycalc/main.py index 5eb6467..387788a 100755 --- a/final_task/pycalc/main.py +++ b/final_task/pycalc/main.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python3 """ This module allows you to work with the functionality of calculator """ import sys -from pycalc.core import args -from pycalc.core.calculator import calculator +from .core import args +from .core.calculator import calculator def main(): diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index 512db61..7d7cfdd 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -1,2 +1,101 @@ import unittest +import ddt import math +from core.calculator_helper import ( + is_number, may_unary_operator, is_unary_operator, + check_right_associativity, is_callable, check_brackets_balance, + check_empty_expression, PycalcError +) + +from core.calculator import ( + split_operands, implicit_multiplication, may_valid_operation, + split_arguments, process_func_or_const, final_execution, + import_modules, execute_comparison, get_length_operator, check_valid_spaces +) + + +@ddt.ddt +class TestCalculatorHelper(unittest.TestCase): + + @ddt.data('12', '12.5', '12.', '.5') + def test_is_number_true(self, value): + self.assertTrue(is_number(value)) + + @ddt.data('qwerty', '.') + def test_is_number_false(self, value): + self.assertFalse(is_number(value)) + + def test_may_unary_operator_true(self): + self.assertTrue(may_unary_operator('+')) + + def test_may_unary_operator_false(self): + self.assertFalse(may_unary_operator('^')) + + def test_is_unary_operator_true(self): + self.assertTrue(is_unary_operator('u+')) + + def test_is_unary_operator_false(self): + self.assertFalse(is_unary_operator('+')) + + def test_check_right_associativity_true(self): + self.assertTrue(check_right_associativity('^')) + + def test_is_callable_true(self): + self.assertTrue(is_callable('sin', math)) + + def test_is_callable_false(self): + self.assertFalse(is_callable('pi', math)) + + def test_check_brackets_balance(self): + with self.assertRaises(PycalcError): + check_brackets_balance('(5+3') + + def test_check_empty_expression(self): + with self.assertRaises(PycalcError): + check_empty_expression('') + + +class TestCalculator(unittest.TestCase): + + def test_split_operands(self): + self.assertEqual(split_operands('5pie'), ['5', 'pi', 'e']) + + def test_implicit_multiplication(self): + self.assertEqual(implicit_multiplication('5pie'), '5*pi*e') + + def test_may_valid_operation(self): + self.assertFalse(may_valid_operation('^', 'u-')) + + def test_split_arguments(self): + self.assertEqual(split_arguments('5,sin(2),sin(sin(5))'), ['5', 'sin(2)', 'sin(sin(5))']) + + def test_process_func_or_const(self): + self.assertEqual(process_func_or_const('sin', 'sin(sin(5)+6)', 3, math), (math.sin(math.sin(5) + 6), 12)) + + def test_execute_operation(self): + self.assertEqual(final_execution(['*'], [5, 7]), 35) + + def test_final_execution_comparison(self): + self.assertTrue(final_execution(['<'], [5, 7])) + + def test_execute_operation_raise(self): + with self.assertRaises(PycalcError): + final_execution(['^'], [-5, .5]) + + def test_import_modules(self): + with self.assertRaises(PycalcError): + import_modules(['module']) + + def test_execute_comparison(self): + self.assertFalse(execute_comparison([5, 8, 11, 25], ['>', '<', '>='])) + + def test_get_length_operator(self): + self.assertEqual(get_length_operator('5+5', 1), 1) + + def test_check_valid_spaces(self): + with self.assertRaises(PycalcError): + check_valid_spaces('5 5') + + +if __name__ == '__main__': + unittest.main() diff --git a/requirements.txt b/requirements.txt index e64ccad..47ae30f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pycodestyle==2.4.0 nose==1.3.7 coverage==4.5.1 termcolor==1.1.0 +ddt==1.2.0 \ No newline at end of file From 91fd3399186584a634f592dff889849361f57782 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sat, 24 Nov 2018 03:55:55 +0300 Subject: [PATCH 09/17] fix bug --- final_task/pycalc/tests/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index 7d7cfdd..89e2375 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -1,13 +1,13 @@ import unittest import ddt import math -from core.calculator_helper import ( +from pycalc.core.calculator_helper import ( is_number, may_unary_operator, is_unary_operator, check_right_associativity, is_callable, check_brackets_balance, check_empty_expression, PycalcError ) -from core.calculator import ( +from pycalc.core.calculator import ( split_operands, implicit_multiplication, may_valid_operation, split_arguments, process_func_or_const, final_execution, import_modules, execute_comparison, get_length_operator, check_valid_spaces From f504e6ddc6e950b8e96ad16b9b21b2ae4dafb8dc Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sat, 24 Nov 2018 04:19:02 +0300 Subject: [PATCH 10/17] add tests --- final_task/pycalc/tests/test.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index 89e2375..b3245d0 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -1,13 +1,13 @@ import unittest import ddt import math -from pycalc.core.calculator_helper import ( +from core.calculator_helper import ( is_number, may_unary_operator, is_unary_operator, check_right_associativity, is_callable, check_brackets_balance, check_empty_expression, PycalcError ) -from pycalc.core.calculator import ( +from core.calculator import ( split_operands, implicit_multiplication, may_valid_operation, split_arguments, process_func_or_const, final_execution, import_modules, execute_comparison, get_length_operator, check_valid_spaces @@ -46,9 +46,10 @@ def test_is_callable_true(self): def test_is_callable_false(self): self.assertFalse(is_callable('pi', math)) - def test_check_brackets_balance(self): + @ddt.data('(5+3', ')5+3(') + def test_check_brackets_balance(self, value): with self.assertRaises(PycalcError): - check_brackets_balance('(5+3') + check_brackets_balance(value) def test_check_empty_expression(self): with self.assertRaises(PycalcError): @@ -60,9 +61,16 @@ class TestCalculator(unittest.TestCase): def test_split_operands(self): self.assertEqual(split_operands('5pie'), ['5', 'pi', 'e']) + def test_split_operands_raise(self): + with self.assertRaises(PycalcError): + split_operands('5abc') + def test_implicit_multiplication(self): self.assertEqual(implicit_multiplication('5pie'), '5*pi*e') + def test_implicit_multiplication_brackets(self): + self.assertEqual(implicit_multiplication('(5+5)5'), '(5+5)*5') + def test_may_valid_operation(self): self.assertFalse(may_valid_operation('^', 'u-')) From 9f96f03f4b1d413d23938fde42b69608ee7fc410 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sat, 24 Nov 2018 04:20:18 +0300 Subject: [PATCH 11/17] fix bug --- final_task/pycalc/tests/test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index b3245d0..fa42cff 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -1,13 +1,13 @@ import unittest import ddt import math -from core.calculator_helper import ( +from pycalc.core.calculator_helper import ( is_number, may_unary_operator, is_unary_operator, check_right_associativity, is_callable, check_brackets_balance, check_empty_expression, PycalcError ) -from core.calculator import ( +from pycalc.core.calculator import ( split_operands, implicit_multiplication, may_valid_operation, split_arguments, process_func_or_const, final_execution, import_modules, execute_comparison, get_length_operator, check_valid_spaces From 19ef8e3cc6711a6a3810cfbde21d0aafb997c26e Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sat, 1 Dec 2018 02:29:50 +0300 Subject: [PATCH 12/17] code refactor --- final_task/pycalc/core/calculator.py | 36 ++++++++++----------- final_task/pycalc/core/calculator_helper.py | 8 ++--- final_task/pycalc/main.py | 4 +-- final_task/pycalc/tests/test.py | 36 ++++++++++----------- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 9c0b818..126b7f1 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -10,7 +10,7 @@ import operator as op from .calculator_helper import PycalcError from .calculator_helper import ( - is_number, may_unary_operator, is_unary_operator, check_right_associativity, is_callable, + check_is_number, check_may_unary_operator, check_is_unary_operator, check_right_associativity, check_is_callable, check_empty_expression, check_brackets_balance ) @@ -70,7 +70,7 @@ def split_operands(merged_operand): if position < 0: raise PycalcError('Unexpected operand') current_string = merged_operand[:position] - if get_module(current_string) or is_number(current_string): + if get_module(current_string) or check_is_number(current_string): operands.append(current_string) merged_operand = merged_operand[position:] position = len(merged_operand) @@ -79,7 +79,7 @@ def split_operands(merged_operand): return operands -def implicit_multiplication(expression): +def do_implicit_multiplication(expression): """ Finds places where multiplication signs are missing and inserts them there :param expression @@ -109,8 +109,8 @@ def implicit_multiplication(expression): module = get_module(operand) is_call = False if module: - is_call = is_callable(operand, module) - if operand and is_number(operand) or module: + is_call = check_is_callable(operand, module) + if operand and check_is_number(operand) or module: j = i - len(operand) - 1 if j > 0: if j and j != i and expression[j] == ')': @@ -124,13 +124,13 @@ def implicit_multiplication(expression): return ''.join(expression) -def may_valid_operation(last_operator, current_operator): +def check_may_valid_operation(last_operator, current_operator): """ :param last_operator :param current_operator :return: True if """ - if is_unary_operator(current_operator) and get_priority(current_operator) < get_priority(last_operator): + if check_is_unary_operator(current_operator) and get_priority(current_operator) < get_priority(last_operator): return return True @@ -207,7 +207,7 @@ def process_func_or_const(operand, expression, operator_end_position, module): args = [calculate(arg) for arg in raw_arguments] try: func_result = getattr(module, operand)(*args) - if not is_number(func_result): + if not check_is_number(func_result): raise PycalcError('Unsupported function result') return func_result, operator_end_position except (TypeError, ValueError) as error: @@ -254,7 +254,7 @@ def check_valid_spaces(expression): elif symbol == ' ': is_space = True elif symbol in OPERATORS or (i < len(expression) - 1 and symbol + expression[i + 1] in OPERATORS): - if is_last_operator and is_space and not may_unary_operator(symbol): + if is_last_operator and is_space and not check_may_unary_operator(symbol): raise PycalcError('Missing operand') else: operator_end_position = i + get_length_operator(expression, i) - 1 @@ -264,7 +264,7 @@ def check_valid_spaces(expression): raise PycalcError('Invalid operator') is_last_operator, is_space = False, False operand, operator_end_position, i = find_operand(expression, i) - if is_number(operand): + if check_is_number(operand): if is_last_number: raise PycalcError('Missing operator') is_last_number = True @@ -304,7 +304,7 @@ def execute_operation(operands, operator): raise PycalcError('Missing operator or operand') -def final_execution(operators, operands): +def do_final_execution(operators, operands): """ Execute operations with operands while list of operators not empty :param operators @@ -379,7 +379,7 @@ def update_operands(expression, operands, i): operand, operator_end_position, i = find_operand(expression, i) module = get_module(operand) - if is_number(operand): + if check_is_number(operand): operand = float(operand) if operand % 1: operands.append(operand) @@ -417,10 +417,10 @@ def update_operators(expression, operator, operators, operands, is_unary, operat operator += expression[i + 1] operator_end_position = i + 1 - if is_unary and may_unary_operator(operator): + if is_unary and check_may_unary_operator(operator): operator = 'u' + operator - if get_priority(operator) >= 1 or (is_unary and may_unary_operator(operator)): + if get_priority(operator) >= 1 or (is_unary and check_may_unary_operator(operator)): while operators and operands: condition_a = not check_right_associativity(operator) condition_b = get_priority(operators[-1]) >= get_priority(operator) @@ -428,7 +428,7 @@ def update_operators(expression, operator, operators, operands, is_unary, operat condition_d = get_priority(operators[-1]) > get_priority(operator) condition = condition_a and condition_b or condition_c and condition_d - if may_valid_operation(operators[-1], operator) and get_priority(operators[-1]) > 1 and condition: + if check_may_valid_operation(operators[-1], operator) and get_priority(operators[-1]) > 1 and condition: execute_operation(operands, operators.pop()) else: break @@ -463,10 +463,10 @@ def calculate(expression): else: operator_end_position, is_unary = update_operands(expression, operands, i) - return final_execution(operators, operands) + return do_final_execution(operators, operands) -def calculator(expression, modules=None): +def do_calculation(expression, modules=None): """ :param expression :param modules @@ -477,7 +477,7 @@ def calculator(expression, modules=None): import_modules(modules) validate_expression(expression) - expression = implicit_multiplication(expression) + expression = do_implicit_multiplication(expression) return calculate(expression) except PycalcError as error: diff --git a/final_task/pycalc/core/calculator_helper.py b/final_task/pycalc/core/calculator_helper.py index e44f6f1..c41d34f 100644 --- a/final_task/pycalc/core/calculator_helper.py +++ b/final_task/pycalc/core/calculator_helper.py @@ -8,7 +8,7 @@ def __init__(self, message): super().__init__('ERROR: ' + str(message)) -def is_number(string): +def check_is_number(string): """ :return: True if the string is a number False otherwise @@ -20,7 +20,7 @@ def is_number(string): return -def may_unary_operator(operator): +def check_may_unary_operator(operator): """ :return: True if the operator may be unary False otherwise @@ -28,7 +28,7 @@ def may_unary_operator(operator): return operator in ['+', '-'] -def is_unary_operator(operator): +def check_is_unary_operator(operator): """ :return: True if the operator is unary False otherwise @@ -44,7 +44,7 @@ def check_right_associativity(operator): return operator == '^' -def is_callable(attribute, module): +def check_is_callable(attribute, module): """ :return: True if the attribute is callable False otherwise diff --git a/final_task/pycalc/main.py b/final_task/pycalc/main.py index 387788a..93c379b 100755 --- a/final_task/pycalc/main.py +++ b/final_task/pycalc/main.py @@ -5,7 +5,7 @@ import sys from .core import args -from .core.calculator import calculator +from .core.calculator import do_calculation def main(): @@ -16,7 +16,7 @@ def main(): """ arguments = args.arg_parser() - answer = calculator(arguments.EXPRESSION, arguments.MODULE) + answer = do_calculation(arguments.EXPRESSION, arguments.MODULE) print(answer) if str(answer).startswith('ERROR'): sys.exit(-1) diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index fa42cff..6dfe3ec 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -2,14 +2,14 @@ import ddt import math from pycalc.core.calculator_helper import ( - is_number, may_unary_operator, is_unary_operator, - check_right_associativity, is_callable, check_brackets_balance, + check_is_number, check_may_unary_operator, check_is_unary_operator, + check_right_associativity, check_is_callable, check_brackets_balance, check_empty_expression, PycalcError ) from pycalc.core.calculator import ( - split_operands, implicit_multiplication, may_valid_operation, - split_arguments, process_func_or_const, final_execution, + split_operands, do_implicit_multiplication, check_may_valid_operation, + split_arguments, process_func_or_const, do_final_execution, import_modules, execute_comparison, get_length_operator, check_valid_spaces ) @@ -19,32 +19,32 @@ class TestCalculatorHelper(unittest.TestCase): @ddt.data('12', '12.5', '12.', '.5') def test_is_number_true(self, value): - self.assertTrue(is_number(value)) + self.assertTrue(check_is_number(value)) @ddt.data('qwerty', '.') def test_is_number_false(self, value): - self.assertFalse(is_number(value)) + self.assertFalse(check_is_number(value)) def test_may_unary_operator_true(self): - self.assertTrue(may_unary_operator('+')) + self.assertTrue(check_may_unary_operator('+')) def test_may_unary_operator_false(self): - self.assertFalse(may_unary_operator('^')) + self.assertFalse(check_may_unary_operator('^')) def test_is_unary_operator_true(self): - self.assertTrue(is_unary_operator('u+')) + self.assertTrue(check_is_unary_operator('u+')) def test_is_unary_operator_false(self): - self.assertFalse(is_unary_operator('+')) + self.assertFalse(check_is_unary_operator('+')) def test_check_right_associativity_true(self): self.assertTrue(check_right_associativity('^')) def test_is_callable_true(self): - self.assertTrue(is_callable('sin', math)) + self.assertTrue(check_is_callable('sin', math)) def test_is_callable_false(self): - self.assertFalse(is_callable('pi', math)) + self.assertFalse(check_is_callable('pi', math)) @ddt.data('(5+3', ')5+3(') def test_check_brackets_balance(self, value): @@ -66,13 +66,13 @@ def test_split_operands_raise(self): split_operands('5abc') def test_implicit_multiplication(self): - self.assertEqual(implicit_multiplication('5pie'), '5*pi*e') + self.assertEqual(do_implicit_multiplication('5pie'), '5*pi*e') def test_implicit_multiplication_brackets(self): - self.assertEqual(implicit_multiplication('(5+5)5'), '(5+5)*5') + self.assertEqual(do_implicit_multiplication('(5+5)5'), '(5+5)*5') def test_may_valid_operation(self): - self.assertFalse(may_valid_operation('^', 'u-')) + self.assertFalse(check_may_valid_operation('^', 'u-')) def test_split_arguments(self): self.assertEqual(split_arguments('5,sin(2),sin(sin(5))'), ['5', 'sin(2)', 'sin(sin(5))']) @@ -81,14 +81,14 @@ def test_process_func_or_const(self): self.assertEqual(process_func_or_const('sin', 'sin(sin(5)+6)', 3, math), (math.sin(math.sin(5) + 6), 12)) def test_execute_operation(self): - self.assertEqual(final_execution(['*'], [5, 7]), 35) + self.assertEqual(do_final_execution(['*'], [5, 7]), 35) def test_final_execution_comparison(self): - self.assertTrue(final_execution(['<'], [5, 7])) + self.assertTrue(do_final_execution(['<'], [5, 7])) def test_execute_operation_raise(self): with self.assertRaises(PycalcError): - final_execution(['^'], [-5, .5]) + do_final_execution(['^'], [-5, .5]) def test_import_modules(self): with self.assertRaises(PycalcError): From 1794931e1841ac4f87a67a1f959ad240bd6d25cc Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 16 Dec 2018 03:08:31 +0300 Subject: [PATCH 13/17] code refactor --- final_task/pycalc/core/args.py | 1 - final_task/pycalc/core/calculator.py | 138 ++++++++++---------- final_task/pycalc/core/calculator_helper.py | 7 +- final_task/pycalc/tests/test.py | 3 + 4 files changed, 77 insertions(+), 72 deletions(-) diff --git a/final_task/pycalc/core/args.py b/final_task/pycalc/core/args.py index 48b6d81..3758ee5 100644 --- a/final_task/pycalc/core/args.py +++ b/final_task/pycalc/core/args.py @@ -7,7 +7,6 @@ def arg_parser(): """ Parse arguments of command line - :return: arguments of command line """ parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 126b7f1..2e998de 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -81,19 +81,19 @@ def split_operands(merged_operand): def do_implicit_multiplication(expression): """ - Finds places where multiplication signs are missing and inserts them there + Finds places where multiplication signs are missed and inserts them there :param expression - :return: expression with multiply signs where they are missing + :return: expression with multiply signs where they are missed """ - operator_end_position = -1 + token_end_position = -1 insert_positions = [] expression = expression.replace(' ', '') expression = re.sub(r'\)\(', ')*(', expression) for i in range(len(expression)): - if operator_end_position >= i: + if token_end_position >= i: continue j = i - operand, operator_end_position, i = find_operand(expression, i) + operand, token_end_position, i = find_operand(expression, i) if operand: operands = split_operands(operand) if not operands: @@ -104,7 +104,7 @@ def do_implicit_multiplication(expression): if i > 0 and expression[i - 1] == ')': splited_operands = '*' + splited_operands expression = expression.replace(operand, splited_operands) - operator_end_position = i + len('*'.join(operands[:-1])) + token_end_position = i + len('*'.join(operands[:-1])) operand = operands[-1] module = get_module(operand) is_call = False @@ -128,7 +128,7 @@ def check_may_valid_operation(last_operator, current_operator): """ :param last_operator :param current_operator - :return: True if + :return: True if operation is valid """ if check_is_unary_operator(current_operator) and get_priority(current_operator) < get_priority(last_operator): return @@ -164,7 +164,7 @@ def split_arguments(arguments_string): split_positions.append(i) for i, position in enumerate(split_positions): - if not i: + if i == 0: arguments.append(arguments_string[:position]) elif i < len(split_positions): arguments.append(arguments_string[split_positions[i - 1] + 1:position]) @@ -176,11 +176,11 @@ def split_arguments(arguments_string): return arguments -def process_func_or_const(operand, expression, operator_end_position, module): +def process_func_or_const(operand, expression, token_end_position, module): """ :param operand :param expression - :param operator_end_position + :param token_end_position :param module :return: the constant or result of the function and position of the last symbol if this is function @@ -189,17 +189,17 @@ def process_func_or_const(operand, expression, operator_end_position, module): count = 0 inner_expression = '' brackets = {'(': 1, ')': -1} - if operator_end_position < len(expression) and expression[operator_end_position] == '(': + if token_end_position < len(expression) and expression[token_end_position] == '(': count += 1 - operator_end_position += 1 + token_end_position += 1 while count: - if expression[operator_end_position] in brackets: - count += brackets[expression[operator_end_position]] + if expression[token_end_position] in brackets: + count += brackets[expression[token_end_position]] if not count: break - inner_expression += expression[operator_end_position] - operator_end_position += 1 - args = [] + inner_expression += expression[token_end_position] + token_end_position += 1 + args = () if inner_expression: raw_arguments = split_arguments(inner_expression) if not raw_arguments[-1]: @@ -209,11 +209,11 @@ def process_func_or_const(operand, expression, operator_end_position, module): func_result = getattr(module, operand)(*args) if not check_is_number(func_result): raise PycalcError('Unsupported function result') - return func_result, operator_end_position + return func_result, token_end_position except (TypeError, ValueError) as error: raise PycalcError(error) - return getattr(module, operand), '' + return getattr(module, operand), None def get_priority(operator): @@ -245,28 +245,28 @@ def check_valid_spaces(expression): :param expression """ is_last_number, is_last_operator, is_space = False, False, False - operator_end_position = -1 + token_end_position = -1 for i, symbol in enumerate(expression): if symbol in ['(', ')']: is_last_number, is_last_operator, is_space = False, False, False - elif operator_end_position >= i: + elif token_end_position >= i: continue elif symbol == ' ': is_space = True elif symbol in OPERATORS or (i < len(expression) - 1 and symbol + expression[i + 1] in OPERATORS): if is_last_operator and is_space and not check_may_unary_operator(symbol): - raise PycalcError('Missing operand') + raise PycalcError('Missed operand') else: - operator_end_position = i + get_length_operator(expression, i) - 1 + token_end_position = i + get_length_operator(expression, i) - 1 is_last_number, is_last_operator, is_space = False, True, False else: if symbol in ['!', '=']: raise PycalcError('Invalid operator') is_last_operator, is_space = False, False - operand, operator_end_position, i = find_operand(expression, i) + operand, token_end_position, i = find_operand(expression, i) if check_is_number(operand): if is_last_number: - raise PycalcError('Missing operator') + raise PycalcError('Missed operator') is_last_number = True else: is_last_number = False @@ -284,7 +284,7 @@ def execute_comparison(operands, operators): if not OPERATORS[operator](operands[i], operands[i + 1]): return False return True - raise PycalcError('Missing operator or operand') + raise PycalcError('Missed operator or operand') def execute_operation(operands, operator): @@ -301,7 +301,7 @@ def execute_operation(operands, operator): left = operands.pop() operands.append(OPERATORS[operator](left, right)) else: - raise PycalcError('Missing operator or operand') + raise PycalcError('Missed operator or operand') def do_final_execution(operators, operands): @@ -312,7 +312,7 @@ def do_final_execution(operators, operands): :return: result of calculation """ if not operands: - raise PycalcError('Missing operator') + raise PycalcError('Missed operator') while operators: if not check_comparison_priority(operators): execute_operation(operands, operators.pop()) @@ -324,7 +324,7 @@ def do_final_execution(operators, operands): raise PycalcError('Negative number cannot be raised to a fractional power') elif len(operands) > 1: - raise PycalcError('Missing operator') + raise PycalcError('Missed operator') answer = operands.pop() if answer % 1 or isinstance(answer, bool): @@ -343,9 +343,9 @@ def find_operand(expression, position): while position < len(expression) and (expression[position].isalnum() or expression[position] == '.'): operand += expression[position] position += 1 - operator_end_position = position - 1 + token_end_position = position - 1 - return operand, operator_end_position, position + return operand, token_end_position, position def get_length_operator(expression, position): @@ -369,53 +369,51 @@ def validate_expression(expression): check_valid_spaces(expression) -def update_operands(expression, operands, i): +def update_operands(expression, operands, index): """ - :param expression: - :param operands: - :param i: - :return: + :param expression + :param operands + :param index: index of symbol with which the operand starts + :return: token's end position index """ - operand, operator_end_position, i = find_operand(expression, i) + operand, token_end_position, index = find_operand(expression, index) module = get_module(operand) if check_is_number(operand): operand = float(operand) - if operand % 1: - operands.append(operand) - else: - operands.append(int(operand)) + if not operand % 1: + operand = int(operand) + operands.append(operand) elif module: - new_operand = process_func_or_const(operand, expression, i, module) - operands.append(new_operand[0]) - if new_operand[1]: - operator_end_position = new_operand[1] + new_operand, tmp_token_end_position = process_func_or_const(operand, expression, index, module) + operands.append(new_operand) + if tmp_token_end_position: + token_end_position = tmp_token_end_position else: raise PycalcError('Unexpected operand') - is_unary = False - return operator_end_position, is_unary + return token_end_position -def update_operators(expression, operator, operators, operands, is_unary, operator_end_position, i): +def update_operators(expression, operator, operators, operands, is_unary, token_end_position, index): """ - :param expression: - :param operator: - :param operators: - :param operands: - :param is_unary: - :param operator_end_position: - :param i: - :return: + :param expression + :param operator + :param operators + :param operands + :param is_unary + :param token_end_position + :param index: index of symbol with which the operator starts + :return: token's end position index """ - if i: - prev_symbol = expression[i - 1] + if not is_unary and index: + prev_symbol = expression[index - 1] if prev_symbol in OPERATORS: is_unary = True - if i < len(expression) - 1 and operator + expression[i + 1] in OPERATORS: - operator += expression[i + 1] - operator_end_position = i + 1 + if index < len(expression) - 1 and operator + expression[index + 1] in OPERATORS: + operator += expression[index + 1] + token_end_position = index + 1 if is_unary and check_may_unary_operator(operator): operator = 'u' + operator @@ -433,7 +431,7 @@ def update_operators(expression, operator, operators, operands, is_unary, operat else: break operators.append(operator) - return operator_end_position + return token_end_position def calculate(expression): @@ -444,10 +442,10 @@ def calculate(expression): check_empty_expression(expression) operands = [] operators = [] - operator_end_position = -1 + token_end_position = -1 is_unary = True - for i, symbol in enumerate(expression): - if operator_end_position >= i: + for index, symbol in enumerate(expression): + if token_end_position >= index: continue elif symbol == '(': operators.append(symbol) @@ -457,17 +455,19 @@ def calculate(expression): execute_operation(operands, operators.pop()) operators.pop() is_unary = False - elif symbol in OPERATORS or (i < len(expression) - 1 and symbol + expression[i + 1] in OPERATORS): - operator_end_position = update_operators(expression, symbol, operators, operands, - is_unary, operator_end_position, i) + elif symbol in OPERATORS or (index < len(expression) - 1 and symbol + expression[index + 1] in OPERATORS): + token_end_position = update_operators(expression, symbol, operators, operands, + is_unary, token_end_position, index) else: - operator_end_position, is_unary = update_operands(expression, operands, i) + token_end_position = update_operands(expression, operands, index) + is_unary = False return do_final_execution(operators, operands) def do_calculation(expression, modules=None): """ + Import user modules and calculate expression :param expression :param modules :return: result of calculation diff --git a/final_task/pycalc/core/calculator_helper.py b/final_task/pycalc/core/calculator_helper.py index c41d34f..44279c9 100644 --- a/final_task/pycalc/core/calculator_helper.py +++ b/final_task/pycalc/core/calculator_helper.py @@ -4,6 +4,9 @@ class PycalcError(Exception): + """ + Class for Pycalc exceptions + """ def __init__(self, message): super().__init__('ERROR: ' + str(message)) @@ -25,7 +28,7 @@ def check_may_unary_operator(operator): :return: True if the operator may be unary False otherwise """ - return operator in ['+', '-'] + return operator in ('+', '-') def check_is_unary_operator(operator): @@ -33,7 +36,7 @@ def check_is_unary_operator(operator): :return: True if the operator is unary False otherwise """ - return operator in ['u+', 'u-'] + return operator in ('u+', 'u-') def check_right_associativity(operator): diff --git a/final_task/pycalc/tests/test.py b/final_task/pycalc/tests/test.py index 6dfe3ec..6b97ae3 100644 --- a/final_task/pycalc/tests/test.py +++ b/final_task/pycalc/tests/test.py @@ -1,3 +1,6 @@ +""" +This module contains tests for the calculator +""" import unittest import ddt import math From 0ccfeba48c5be355a4b76c9f323a080e71e04466 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Sun, 16 Dec 2018 03:20:54 +0300 Subject: [PATCH 14/17] code refactor --- final_task/pycalc/core/calculator.py | 58 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 2e998de..3f716d2 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -89,38 +89,38 @@ def do_implicit_multiplication(expression): insert_positions = [] expression = expression.replace(' ', '') expression = re.sub(r'\)\(', ')*(', expression) - for i in range(len(expression)): - if token_end_position >= i: + for index in range(len(expression)): + if token_end_position >= index: continue - j = i - operand, token_end_position, i = find_operand(expression, i) + tmp_index = index + operand, token_end_position, index = find_operand(expression, index) if operand: operands = split_operands(operand) if not operands: raise PycalcError('Unexpected operand') elif len(operands) > 1: - i = j + index = tmp_index splited_operands = '*'.join(operands) - if i > 0 and expression[i - 1] == ')': + if index > 0 and expression[index - 1] == ')': splited_operands = '*' + splited_operands expression = expression.replace(operand, splited_operands) - token_end_position = i + len('*'.join(operands[:-1])) + token_end_position = index + len('*'.join(operands[:-1])) operand = operands[-1] module = get_module(operand) is_call = False if module: is_call = check_is_callable(operand, module) if operand and check_is_number(operand) or module: - j = i - len(operand) - 1 - if j > 0: - if j and j != i and expression[j] == ')': - insert_positions.append(j + 1 + len(insert_positions)) - j = i - if j < len(expression) and expression[j] == '(' and not is_call: - insert_positions.append(j + len(insert_positions)) + tmp_index = index - len(operand) - 1 + if tmp_index > 0: + if tmp_index and tmp_index != index and expression[tmp_index] == ')': + insert_positions.append(tmp_index + 1 + len(insert_positions)) + tmp_index = index + if tmp_index < len(expression) and expression[tmp_index] == '(' and not is_call: + insert_positions.append(tmp_index + len(insert_positions)) expression = list(expression) - for i in insert_positions: - expression.insert(i, '*') + for index in insert_positions: + expression.insert(index, '*') return ''.join(expression) @@ -157,17 +157,17 @@ def split_arguments(arguments_string): count = 0 split_positions = [] arguments = [] - for i, symbol in enumerate(arguments_string): + for index, symbol in enumerate(arguments_string): if symbol in brackets: count += brackets[symbol] elif symbol == ',' and not count: - split_positions.append(i) + split_positions.append(index) - for i, position in enumerate(split_positions): - if i == 0: + for index, position in enumerate(split_positions): + if index == 0: arguments.append(arguments_string[:position]) - elif i < len(split_positions): - arguments.append(arguments_string[split_positions[i - 1] + 1:position]) + elif index < len(split_positions): + arguments.append(arguments_string[split_positions[index - 1] + 1:position]) if split_positions: arguments.append(arguments_string[split_positions[-1] + 1:]) @@ -246,24 +246,24 @@ def check_valid_spaces(expression): """ is_last_number, is_last_operator, is_space = False, False, False token_end_position = -1 - for i, symbol in enumerate(expression): + for index, symbol in enumerate(expression): if symbol in ['(', ')']: is_last_number, is_last_operator, is_space = False, False, False - elif token_end_position >= i: + elif token_end_position >= index: continue elif symbol == ' ': is_space = True - elif symbol in OPERATORS or (i < len(expression) - 1 and symbol + expression[i + 1] in OPERATORS): + elif symbol in OPERATORS or (index < len(expression) - 1 and symbol + expression[index + 1] in OPERATORS): if is_last_operator and is_space and not check_may_unary_operator(symbol): raise PycalcError('Missed operand') else: - token_end_position = i + get_length_operator(expression, i) - 1 + token_end_position = index + get_length_operator(expression, index) - 1 is_last_number, is_last_operator, is_space = False, True, False else: if symbol in ['!', '=']: raise PycalcError('Invalid operator') is_last_operator, is_space = False, False - operand, token_end_position, i = find_operand(expression, i) + operand, token_end_position, index = find_operand(expression, index) if check_is_number(operand): if is_last_number: raise PycalcError('Missed operator') @@ -280,8 +280,8 @@ def execute_comparison(operands, operators): True otherwise """ if len(operands) == len(operators) + 1: - for i, operator in enumerate(operators): - if not OPERATORS[operator](operands[i], operands[i + 1]): + for index, operator in enumerate(operators): + if not OPERATORS[operator](operands[index], operands[index + 1]): return False return True raise PycalcError('Missed operator or operand') From 8dbbadeb768a5a56ed7be3756141966e04e98c46 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Thu, 20 Dec 2018 04:13:23 +0300 Subject: [PATCH 15/17] code refactor --- final_task/pycalc/core/calculator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 3f716d2..819e1aa 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -247,7 +247,7 @@ def check_valid_spaces(expression): is_last_number, is_last_operator, is_space = False, False, False token_end_position = -1 for index, symbol in enumerate(expression): - if symbol in ['(', ')']: + if symbol in ('(', ')'): is_last_number, is_last_operator, is_space = False, False, False elif token_end_position >= index: continue @@ -260,7 +260,7 @@ def check_valid_spaces(expression): token_end_position = index + get_length_operator(expression, index) - 1 is_last_number, is_last_operator, is_space = False, True, False else: - if symbol in ['!', '=']: + if symbol in ('!', '='): raise PycalcError('Invalid operator') is_last_operator, is_space = False, False operand, token_end_position, index = find_operand(expression, index) From ab336d8ac194493a2068d2f7b8a936afaf6f1e9c Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Thu, 20 Dec 2018 05:33:33 +0300 Subject: [PATCH 16/17] fix bug --- final_task/pycalc/core/calculator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 819e1aa..1bbe3fa 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -89,8 +89,10 @@ def do_implicit_multiplication(expression): insert_positions = [] expression = expression.replace(' ', '') expression = re.sub(r'\)\(', ')*(', expression) - for index in range(len(expression)): + index = 0 + while index < len(expression) - 1: if token_end_position >= index: + index += 1 continue tmp_index = index operand, token_end_position, index = find_operand(expression, index) @@ -118,6 +120,7 @@ def do_implicit_multiplication(expression): tmp_index = index if tmp_index < len(expression) and expression[tmp_index] == '(' and not is_call: insert_positions.append(tmp_index + len(insert_positions)) + index += 1 expression = list(expression) for index in insert_positions: expression.insert(index, '*') From bf0a6456f683264cc6b5b45c906214d3a0528919 Mon Sep 17 00:00:00 2001 From: Korotynski Alexey Date: Thu, 20 Dec 2018 05:45:16 +0300 Subject: [PATCH 17/17] fix bug --- final_task/pycalc/core/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/pycalc/core/calculator.py b/final_task/pycalc/core/calculator.py index 1bbe3fa..bb7cd6c 100644 --- a/final_task/pycalc/core/calculator.py +++ b/final_task/pycalc/core/calculator.py @@ -90,7 +90,7 @@ def do_implicit_multiplication(expression): expression = expression.replace(' ', '') expression = re.sub(r'\)\(', ')*(', expression) index = 0 - while index < len(expression) - 1: + while index <= len(expression) - 1: if token_end_position >= index: index += 1 continue