From ed1963c8ac7d601169cfa5f5d1ecca71852e9aa9 Mon Sep 17 00:00:00 2001 From: "2690736@gmail.com" <2690736@gmail.com> Date: Thu, 22 Nov 2018 23:35:26 +0300 Subject: [PATCH 1/4] calc is work --- final_task/pycalc/__init__.py | 0 final_task/pycalc/pycalc.py | 426 +++++++++++++++++++++++++++++++ final_task/pycalc/test_pycalc.py | 135 ++++++++++ final_task/setup.py | 12 + 4 files changed, 573 insertions(+) create mode 100644 final_task/pycalc/__init__.py create mode 100644 final_task/pycalc/pycalc.py create mode 100644 final_task/pycalc/test_pycalc.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/pycalc.py b/final_task/pycalc/pycalc.py new file mode 100644 index 0000000..7d53cbf --- /dev/null +++ b/final_task/pycalc/pycalc.py @@ -0,0 +1,426 @@ +import argparse +import math +import re +import operator + + +class WrongBracketsBalance(Exception): + pass + + +class SpaceBetweenOperands(Exception): + pass + + +class SpaceIn2ElementOperators(Exception): + pass + + +class TokenParseException(Exception): + pass + + +class WrongToken(ValueError): + pass + + +class WrongOperandsCount(ValueError): + pass + + +operators = { + '+': [0, operator.add], + '-': [0, operator.sub], + '*': [1, operator.mul], + '/': [1, operator.truediv], + '^': [2, operator.pow], + '//': [0.5, operator.floordiv], + '%': [0.5, operator.mod], +} + +prefix_function = { + 'sin': [0, math.sin, 1], + 'cos': [0, math.cos, 1], + 'tan': [0, math.tan, 1], + 'exp': [0, math.exp, 1], + 'acos': [0, math.acos, 1], + 'asin': [0, math.asin, 1], + 'atan': [0, math.atan, 1], + 'sqrt': [0, math.sqrt, 1], + 'log': [0, math.log, 2], + 'log10': [0, math.log10, 1], + 'loglp': [0, math.log1p, 1], + 'factorial': [0, math.factorial, 1], + 'pow': [0, math.pow, 2], + 'abs': [0, abs, 1], + 'round': [0, round, 1], +} + +constants = { + 'pi': math.pi, + 'e': math.e, +} + +comparisons = { + '<': operator.lt, + '>': operator.gt, + '<=': operator.le, + '>=': operator.ge, + '==': operator.eq, + '!=': operator.ne, +} + +unary = { + '-': operator.neg, + '+': operator.pos, +} + + +def get_token(expression='', result_lst=None): + """ + getting expression in string format and split by simple tokens using recursion + + :param expression: expression in string format + :param result_lst: None if first function call, else part of expression for ex.'(-1+5)' + :return: result_lst contains all tokens as list elements for ex. ['(', '-', '1', '+', '5', ')'] + """ + if result_lst is None: + expression = expression.lower().replace(' ', '') + result_lst = [] + el = re.match(r'\)|\(|-\d+\.?\d*|\d+\.\d+|(--)|-\w*|[-+*/,^]|\.\d+|\w+|(\W{1,2})|[^\d]\w+|\d+|\D+', expression) + try: + el.group() + except: + raise TokenParseException('ERROR: wrong expression') + if el.group()[-1] == '(': + el = re.match(r'\D{1,1}', expression) + result_lst.append(el.group()) + elif el.group() == '--': + result_lst.append('+') + elif el.group()[0] == '.': + result_lst.append('0' + el.group()) + else: + result_lst.append(el.group()) + if len(el.group()) < len(expression): + return get_token(expression[el.end():], result_lst) + else: + return result_lst + + +def implicit_mul(tokens): + """ + gets list contain tokens verify and add if need multiply operator + + :param tokens: list with tokens for ex.['(', '5', ')', '(', '1', ')' ] + :return: list with tokens and * if add it ['(', '5', ')', '*', '(', '1', ')' ] + """ + while True: + for index, token in enumerate(tokens): + if token == '(' and index != 0: + if (type(tokens[index - 1]) is float) or (tokens[index - 1] is ')'): + tokens.insert(index, '*') + continue + elif token == ')' and index != len(tokens) - 1: + # print(tokens[index+1]) + if type(tokens[index + 1]) is float: + tokens.insert(index + 1, '*') + continue + return tokens + + +def verify_to_elements_operator(expression): + to_el_operators_separate_whitespace = [ + '< =', '> =', '= =', '* *', '/ /' + ] + for el in to_el_operators_separate_whitespace: + if el in expression: + raise SpaceIn2ElementOperators('ERROR: {} is wrong operators'.format(el)) + else: + continue + + +def check_space_between_operands(expression=''): + ''' + + get expression and verify it has space between 2 operands + + :param expression: + :return: + ''' + expression = expression.rstrip().lstrip() + for index, el in enumerate(expression): + if el == ' ': + try: + prev = float(expression[index - 1]) + next_ = float(expression[index + 1]) + except Exception as e: + continue + if prev and next_: + raise SpaceBetweenOperands('ERROR: space between operands') + + +def brackets_is_balanced(expression): + """ + verify brackets is balanced or not + + :param expression: in str or list format for ex. ['(', '1', '+', '5', ')'] + :return: True if brackets is balanced else False + """ + count_open_brackets = expression.count('(') + count_close_brackets = expression.count(')') + return count_open_brackets == count_close_brackets + + +def calculate_expression(expression): + """ + calculate expression in reverse Polish notation + + :param expression: list contains tokens in reverse Polish notation ['1', '5', '+'] + :return: result + """ + stack = [] + try: + while expression: + token = expression.pop(0) + if type(token) is float: + stack.append(token) + + elif token in operators: + if len(stack) == 1: + func = unary[token] + operands = (stack.pop(-1),) + stack.append(func(*operands)) + + elif len(stack) == 0: + func = unary[token] + for i, el in enumerate(expression): + if type(el) is not float: + continue + else: + operands = (expression.pop(i),) + res = func(*operands) + expression.insert(i, res) + else: + func = operators[token][1] + operands = (stack.pop(-2), stack.pop(-1)) + stack.append(func(*operands)) + + elif token in prefix_function: + func = prefix_function[token] + if func[2] == 1: + operands = (stack.pop(-1),) + stack.append(func[1](*operands)) + elif func[2] == 2: + try: + operands = (stack.pop(-2), stack.pop(-1)) + except: + operands = (stack.pop(-1),) + stack.append(func[1](*operands)) + if len(stack) == 1: + return stack[0] + else: + raise WrongOperandsCount('ERROR: Wrong operands count') + except Exception as e: + return e + + +def convert_num_and_const_to_float(expression): + """ + converts constants and numbers to float type + :param expression: may be in list format with simple tokens for ex. ['(', '-', '1', '+', '5', ')'] + :return: + """ + for index, el in enumerate(expression): + try: + if el in constants: + expression[index] = constants[el] + elif (el[0] == '-') and (el[1:] in constants): + expression[index] = operator.neg(constants[el[1:]]) + elif (el[0] == '-' and len(el) != 1) and (type(expression[index - 1]) is float): + expression.insert(index, '-') + element = float(expression.pop(index + 1)) * (-1) + expression.insert(index + 1, element) + elif (el[0] == '-' and len(el) != 1) and (el[1:] in prefix_function): + if expression[index - 1] == '(': + expression.insert(index, 0.0) + expression.insert(index + 1, '-') + expression.insert(index + 2, expression.pop(index + 2)[1:]) + else: + expression.insert(index, '-') + expression.insert(index + 1, expression.pop(index + 1)[1:]) + else: + try: + expression[index] = float(el.replace(',', '.')) if type(el) != float else el + except ValueError: + pass + except TypeError: + continue + return expression + + +def parse_to_reverse_polish_notation(tokens, postfix_expression=None, stack=None): + """ + convert infix expression format to reverse Polish notation expression + :param tokens: list contains simple tokens: numbers is float operators is str + :param postfix_expression: this list will be return + :param stack: tmp list for stack operators + :return: expression in reverse Polish notation + """ + tokens = convert_num_and_const_to_float(tokens) + + if tokens[-1] in operators or tokens[-1] in prefix_function: + raise Exception('ERROR: wrong expression') + + if (postfix_expression is None) and (stack is None): + postfix_expression, stack = [], [] + + while len(tokens): + token = tokens[0] + + if type(token) is float: + postfix_expression.append(tokens.pop(0)) + + elif token in prefix_function: + stack.append(tokens.pop(0)) + elif token is ',': + while True: + try: + if stack[-1] is not '(': + postfix_expression.append(stack.pop()) + else: + tokens.pop(0) + break + except IndexError: + raise WrongBracketsBalance('ERROR: brackets are not balanced') + elif token in operators: + while True: + if len(stack) is 0: + stack.append(tokens.pop(0)) + break + else: + tmp = stack[-1] + if tmp in operators: + # if (token in operators) and (operators[token][0] <= operators[tmp][0]): + # postfix_expression.append(stack.pop()) + if ((operators[token][0] <= operators[tmp][0]) and (token != '^')): + postfix_expression.append(stack.pop()) + elif (operators[token][0] > operators[tmp][0]): + stack.append(tokens.pop(0)) + break + else: + stack.append(tokens.pop(0)) + break + elif tmp in prefix_function: + postfix_expression.append(stack.pop()) + elif (len(stack) is 0) or (tmp is '('): + stack.append(tokens.pop(0)) + break + elif token == '(': + stack.append(tokens.pop(0)) + + elif token == ')': + tokens.pop(0) + try: + index = len(stack) - 1 + while True: + if stack[index] != '(' and index != 0: + postfix_expression.append(stack.pop()) + index -= 1 + if len(stack) is 0: + raise WrongBracketsBalance + else: + stack.pop() + break + except Exception as e: + print(e) + else: + raise WrongToken('ERROR: {token} is not correct element of expression'.format(token=token)) + while len(stack): + postfix_expression.append(stack.pop()) + + return postfix_expression + + +def split_by_comparison(expression): + """ + split expressin by comparison operators + :param expression: expression type is string + :return: tuple contains parts of expression after splitting and list of comp. operators + """ + expression = re.sub(r'\s', '', expression) + exp = re.split(r'<=|>=|\!\=|==|<|>', expression) + comparisons_operator = re.findall(r'<=|>=|!=|==|<|>', expression) + return exp, comparisons_operator + + +def comparison_expressions(expresions, comparisons_lst): + """ + comparison some expressions + :param expresions: + :param comparisons_lst: + :return: bool value + """ + result = [] + while len(expresions) != 1: + last_el = expresions.pop() + befor_last_el = expresions[-1] + comparison = comparisons[comparisons_lst.pop()] + result.append(comparison(befor_last_el, last_el)) + while len(result) != 1: + last_el = result.pop() + befor_last_el = result.pop() + result.append(operator.and_(befor_last_el, last_el)) + return result[0] + + +def from_str_to_result(expression): + """ + convert and calculate expression step by step + :param expression: expression type is str + :return: result or raise Exception if expression has error + """ + if not brackets_is_balanced(expression): + raise WrongBracketsBalance('ERROR: brackets are not balanced') + + expression = get_token(expression) + expression = implicit_mul(expression) + expression = parse_to_reverse_polish_notation(expression) + try: + expression = calculate_expression(expression) + except ZeroDivisionError as e: + print('ERROR: {exception}'.format(exception=e)) + return expression + + +def calculate_and_comparison(expression_list, comparison_lst): + """ + calculate and comarison all expressions in expression_list + :param expression_list: list contains expressions to calculate and comparison + :param comparison_lst: list comparison operators + :return: result + """ + calculate_expression_list = [from_str_to_result(el) for el in expression_list] + return comparison_expressions(calculate_expression_list, comparison_lst) + + +def main(): + parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('EXPRESSION', action="store", help="expression string to evaluate") + args = parser.parse_args() + if (args.EXPRESSION) is not None: + try: + expression = args.EXPRESSION + verify_to_elements_operator(expression) + check_space_between_operands(expression) + expressions, comparison = split_by_comparison(expression) + if not comparison: + print(from_str_to_result(expressions[0])) + else: + print(calculate_and_comparison(expressions, comparison)) + except Exception as exception: + print(exception) + return exception + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/final_task/pycalc/test_pycalc.py b/final_task/pycalc/test_pycalc.py new file mode 100644 index 0000000..250622d --- /dev/null +++ b/final_task/pycalc/test_pycalc.py @@ -0,0 +1,135 @@ +import unittest + +import math + +from .pycalc import ( + get_token, + parse_to_reverse_polish_notation, + calculate_expression, + split_by_comparison, + comparison_expressions, + calculate_and_comparison, + from_str_to_result, + check_space_between_operands, + verify_to_elements_operator, + convert_num_and_const_to_float + ) + + +#import Exceptions +from .pycalc import ( + WrongBracketsBalance, + SpaceBetweenOperands, + SpaceIn2ElementOperators, + WrongToken, +) + + +class Test(unittest.TestCase): + def setUp(self): + self.expression = get_token('(343+pi^3)/(e+sin(3-11))') + self.reverse_polish_notation = parse_to_reverse_polish_notation(self.expression) + self.calculate = calculate_expression(self.reverse_polish_notation) + self.split_by_comparison_result = split_by_comparison('(343 + pi ^ 3) < (e + sin(3 - 11)) != (845/pi*12^2)') + self.calculate_and_comparison = calculate_and_comparison( + *split_by_comparison('(343 + pi ^ 3) < (e + sin(3 - 11)) != (845/pi*12^2)') + ) + + def test_expression_get_token(self): + self.assertEqual( + get_token('(343+pi^3)/3(e+sin(3-11))'), + ['(', '343', '+', 'pi', '^', '3', ')', '/', '3', '(', 'e', '+', 'sin', '(', '3', '-11', ')', ')'] + ) + + def test_expression_get_token_with_unary(self): + self.assertEqual( + get_token('-343+pi^3'), + ['-343', '+', 'pi', '^', '3'] + ) + + def test_expression_get_token_with_duble_minus(self): + self.assertEqual( + get_token('--343+pi^3'), + ['+', '343', '+', 'pi', '^', '3'] + ) + + def test_expression_polish_notation(self): + rpn = parse_to_reverse_polish_notation(get_token('(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)')) + self.assertEqual( + rpn, + [2.0, 3.141592653589793, 3.141592653589793, '/', 2.718281828459045, 2.718281828459045, + '/', '+', 2.0, 0.0, '^', '+', '^', 1.0, 3.0, '/', '^'] + ) + + + def test_from_string_co_result(self): + self.assertEqual(from_str_to_result('--343+pi^3'), (--343+math.pi**3)) + + def test_calculate_expr(self): + self.assertEqual( + self.calculate, + 216.3231970514297 + ) + + def test_check_for_negativ_and_constants(self): + self.assertEqual(convert_num_and_const_to_float(['(', '-1', '+', '5', '+', 'pi', ')']), + ['(', -1.0, '+', 5.0, '+', math.pi, ')']) + + def test_split_by_comparison(self): + self.assertEqual( + self.split_by_comparison_result, + (['(343+pi^3)', '(e+sin(3-11))', '(845/pi*12^2)'], ['<', '!=']) + ) + + def test_calculate_and_comparison(self): + self.assertFalse( + self.calculate_and_comparison + ) + + def test_comparison_eq(self): + self.assertTrue( + comparison_expressions([5, 5], ['==']) + ) + + def test_comparison_ge(self): + self.assertTrue( + comparison_expressions([8, 5], ['>=']) + ) + + def test_comparison_le(self): + self.assertTrue( + comparison_expressions([5, 8], ['<=']) + ) + + def test_comparison_ne_true(self): + self.assertTrue( + comparison_expressions([5, 8], ['!=']) + ) + + def test_comparison_ne_false(self): + self.assertFalse( + comparison_expressions([5, 5], ['!=']) + ) + + def test_from_str_to_result(self): + self.assertEqual(from_str_to_result('(2.0^(pi/pi+e/e+2.0^0.0))'), 8) + + def test_error_raising_brackets(self): + with self.assertRaises(WrongBracketsBalance): + from_str_to_result('((1+15)') + + def test_error_raising_space_between_operands(self): + with self.assertRaises(SpaceBetweenOperands): + check_space_between_operands("1 + 1 2 3 4 5 6 ") + + def test_error_raising_space_2el_operators(self): + with self.assertRaises(SpaceIn2ElementOperators): + verify_to_elements_operator('6 < = 6') + + def test_wrong_token(self): + with self.assertRaises(WrongToken): + parse_to_reverse_polish_notation(['(', '-1', '+', '5', '+', 'qwe', ')']) + + def test_zero_divizion(self): + self.failureException(calculate_expression([ '0', '5.0', '/']), ZeroDivisionError) + diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..9d4c9cc 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages +from os.path import join, dirname + +setup( + name='pycalc', + version='1.0', + packages=find_packages(), + entry_points={ + 'console_scripts': + ['pycalc = pycalc.pycalc:main'] + }, +) From ff9d3ee3da8488912eb0e2305cdc0dba2d0080ec Mon Sep 17 00:00:00 2001 From: "2690736@gmail.com" <2690736@gmail.com> Date: Thu, 22 Nov 2018 23:55:18 +0300 Subject: [PATCH 2/4] calc is work --- final_task/pycalc/pycalc.py | 23 +++++++++++------------ final_task/pycalc/test_pycalc.py | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index 7d53cbf..d966cf1 100644 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -87,7 +87,8 @@ def get_token(expression='', result_lst=None): if result_lst is None: expression = expression.lower().replace(' ', '') result_lst = [] - el = re.match(r'\)|\(|-\d+\.?\d*|\d+\.\d+|(--)|-\w*|[-+*/,^]|\.\d+|\w+|(\W{1,2})|[^\d]\w+|\d+|\D+', expression) + el = re.match(r'\)|\(|-\d+\.?\d*|\d+\.\d+|(--)|-\w*|[-+*/,^]|\.\d+|\w+|(\W{1,2})|[^\d]\w+|\d+|\D+', + expression) try: el.group() except: @@ -140,20 +141,20 @@ def verify_to_elements_operator(expression): def check_space_between_operands(expression=''): - ''' + """ get expression and verify it has space between 2 operands :param expression: :return: - ''' + """ expression = expression.rstrip().lstrip() for index, el in enumerate(expression): if el == ' ': try: prev = float(expression[index - 1]) next_ = float(expression[index + 1]) - except Exception as e: + except Exception: continue if prev and next_: raise SpaceBetweenOperands('ERROR: space between operands') @@ -213,7 +214,7 @@ def calculate_expression(expression): elif func[2] == 2: try: operands = (stack.pop(-2), stack.pop(-1)) - except: + except Exception: operands = (stack.pop(-1),) stack.append(func[1](*operands)) if len(stack) == 1: @@ -300,11 +301,9 @@ def parse_to_reverse_polish_notation(tokens, postfix_expression=None, stack=None else: tmp = stack[-1] if tmp in operators: - # if (token in operators) and (operators[token][0] <= operators[tmp][0]): - # postfix_expression.append(stack.pop()) - if ((operators[token][0] <= operators[tmp][0]) and (token != '^')): + if (operators[token][0] <= operators[tmp][0]) and (token != '^'): postfix_expression.append(stack.pop()) - elif (operators[token][0] > operators[tmp][0]): + elif operators[token][0] > operators[tmp][0]: stack.append(tokens.pop(0)) break else: @@ -348,7 +347,7 @@ def split_by_comparison(expression): :return: tuple contains parts of expression after splitting and list of comp. operators """ expression = re.sub(r'\s', '', expression) - exp = re.split(r'<=|>=|\!\=|==|<|>', expression) + exp = re.split(r'<=|>=|!=|==|<|>', expression) comparisons_operator = re.findall(r'<=|>=|!=|==|<|>', expression) return exp, comparisons_operator @@ -407,7 +406,7 @@ def main(): parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') parser.add_argument('EXPRESSION', action="store", help="expression string to evaluate") args = parser.parse_args() - if (args.EXPRESSION) is not None: + if args.EXPRESSION is not None: try: expression = args.EXPRESSION verify_to_elements_operator(expression) @@ -423,4 +422,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/final_task/pycalc/test_pycalc.py b/final_task/pycalc/test_pycalc.py index 250622d..4f3aaa8 100644 --- a/final_task/pycalc/test_pycalc.py +++ b/final_task/pycalc/test_pycalc.py @@ -72,7 +72,7 @@ def test_calculate_expr(self): ) def test_check_for_negativ_and_constants(self): - self.assertEqual(convert_num_and_const_to_float(['(', '-1', '+', '5', '+', 'pi', ')']), + self.assertEqual(convert_num_and_const_to_float(['(', '-1', '+', '5,0', '+', 'pi', ')']), ['(', -1.0, '+', 5.0, '+', math.pi, ')']) def test_split_by_comparison(self): From 587853c765d9c8564e6568c3c2264b098aae0d70 Mon Sep 17 00:00:00 2001 From: "2690736@gmail.com" <2690736@gmail.com> Date: Fri, 23 Nov 2018 00:02:47 +0300 Subject: [PATCH 3/4] calc is work PEP8 style --- final_task/pycalc/pycalc.py | 3 +-- final_task/pycalc/test_pycalc.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index d966cf1..6cc91e1 100644 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -91,7 +91,7 @@ def get_token(expression='', result_lst=None): expression) try: el.group() - except: + except Exception: raise TokenParseException('ERROR: wrong expression') if el.group()[-1] == '(': el = re.match(r'\D{1,1}', expression) @@ -122,7 +122,6 @@ def implicit_mul(tokens): tokens.insert(index, '*') continue elif token == ')' and index != len(tokens) - 1: - # print(tokens[index+1]) if type(tokens[index + 1]) is float: tokens.insert(index + 1, '*') continue diff --git a/final_task/pycalc/test_pycalc.py b/final_task/pycalc/test_pycalc.py index 4f3aaa8..8a00a5f 100644 --- a/final_task/pycalc/test_pycalc.py +++ b/final_task/pycalc/test_pycalc.py @@ -16,7 +16,7 @@ ) -#import Exceptions +# import Exceptions from .pycalc import ( WrongBracketsBalance, SpaceBetweenOperands, @@ -61,7 +61,6 @@ def test_expression_polish_notation(self): '/', '+', 2.0, 0.0, '^', '+', '^', 1.0, 3.0, '/', '^'] ) - def test_from_string_co_result(self): self.assertEqual(from_str_to_result('--343+pi^3'), (--343+math.pi**3)) @@ -131,5 +130,4 @@ def test_wrong_token(self): parse_to_reverse_polish_notation(['(', '-1', '+', '5', '+', 'qwe', ')']) def test_zero_divizion(self): - self.failureException(calculate_expression([ '0', '5.0', '/']), ZeroDivisionError) - + self.failureException(calculate_expression(['0', '5.0', '/']), ZeroDivisionError) From d6639d772159a7bc809cc8a72a2f9960d35e3dbf Mon Sep 17 00:00:00 2001 From: "2690736@gmail.com" <2690736@gmail.com> Date: Thu, 29 Nov 2018 12:59:36 +0300 Subject: [PATCH 4/4] changes after comments --- final_task/pycalc/pycalc.py | 164 ++++++++++++++++++------------- final_task/pycalc/test_pycalc.py | 18 ++-- 2 files changed, 105 insertions(+), 77 deletions(-) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index 6cc91e1..236339a 100644 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -2,58 +2,63 @@ import math import re import operator +from collections import namedtuple -class WrongBracketsBalance(Exception): +class ErrorWrongBracketsBalance(Exception): pass -class SpaceBetweenOperands(Exception): +class ErrorSpaceBetweenOperands(Exception): pass -class SpaceIn2ElementOperators(Exception): +class ErrorSpaceIn2ElementOperators(Exception): pass -class TokenParseException(Exception): +class ErrorTokenParseException(Exception): pass -class WrongToken(ValueError): +class ErrorWrongToken(ValueError): pass -class WrongOperandsCount(ValueError): +class ErrorWrongOperandsCount(ValueError): pass +operator_ = namedtuple('operator', ['priority', 'function']) + operators = { - '+': [0, operator.add], - '-': [0, operator.sub], - '*': [1, operator.mul], - '/': [1, operator.truediv], - '^': [2, operator.pow], - '//': [0.5, operator.floordiv], - '%': [0.5, operator.mod], + '+': operator_(0, operator.add), + '-': operator_(0, operator.sub), + '*': operator_(1, operator.mul), + '/': operator_(1, operator.truediv), + '^': operator_(2, operator.pow), + '//': operator_(0.5, operator.floordiv), + '%': operator_(0.5, operator.mod), } +prefix_func = namedtuple('prefix_function', ['priority', 'function', 'args_count']) + prefix_function = { - 'sin': [0, math.sin, 1], - 'cos': [0, math.cos, 1], - 'tan': [0, math.tan, 1], - 'exp': [0, math.exp, 1], - 'acos': [0, math.acos, 1], - 'asin': [0, math.asin, 1], - 'atan': [0, math.atan, 1], - 'sqrt': [0, math.sqrt, 1], - 'log': [0, math.log, 2], - 'log10': [0, math.log10, 1], - 'loglp': [0, math.log1p, 1], - 'factorial': [0, math.factorial, 1], - 'pow': [0, math.pow, 2], - 'abs': [0, abs, 1], - 'round': [0, round, 1], + 'sin': prefix_func(0, math.sin, 1), + 'cos': prefix_func(0, math.cos, 1), + 'tan': prefix_func(0, math.tan, 1), + 'exp': prefix_func(0, math.exp, 1), + 'acos': prefix_func(0, math.acos, 1), + 'asin': prefix_func(0, math.asin, 1), + 'atan': prefix_func(0, math.atan, 1), + 'sqrt': prefix_func(0, math.sqrt, 1), + 'log': prefix_func(0, math.log, 2), + 'log10': prefix_func(0, math.log10, 1), + 'loglp': prefix_func(0, math.log1p, 1), + 'factorial': prefix_func(0, math.factorial, 1), + 'pow': prefix_func(0, math.pow, 2), + 'abs': prefix_func(0, abs, 1), + 'round': prefix_func(0, round, 1), } constants = { @@ -87,12 +92,11 @@ def get_token(expression='', result_lst=None): if result_lst is None: expression = expression.lower().replace(' ', '') result_lst = [] - el = re.match(r'\)|\(|-\d+\.?\d*|\d+\.\d+|(--)|-\w*|[-+*/,^]|\.\d+|\w+|(\W{1,2})|[^\d]\w+|\d+|\D+', - expression) + el = re.match(r'\)|\(|-\d+\.?\d*|\d+\.?\d*|(--)|-\w*|[-+*/,^]|\.\d+|\w+|\D+|(\d+)\w', expression) try: el.group() except Exception: - raise TokenParseException('ERROR: wrong expression') + raise ErrorTokenParseException('ERROR: wrong expression') if el.group()[-1] == '(': el = re.match(r'\D{1,1}', expression) result_lst.append(el.group()) @@ -100,6 +104,9 @@ def get_token(expression='', result_lst=None): result_lst.append('+') elif el.group()[0] == '.': result_lst.append('0' + el.group()) + elif el.group() == 'epi': + result_lst.append('e') + result_lst.append('pi') else: result_lst.append(el.group()) if len(el.group()) < len(expression): @@ -108,24 +115,38 @@ def get_token(expression='', result_lst=None): return result_lst -def implicit_mul(tokens): +def check_implicit_mul(tokens): """ gets list contain tokens verify and add if need multiply operator :param tokens: list with tokens for ex.['(', '5', ')', '(', '1', ')' ] :return: list with tokens and * if add it ['(', '5', ')', '*', '(', '1', ')' ] """ - while True: - for index, token in enumerate(tokens): - if token == '(' and index != 0: - if (type(tokens[index - 1]) is float) or (tokens[index - 1] is ')'): - tokens.insert(index, '*') - continue - elif token == ')' and index != len(tokens) - 1: - if type(tokens[index + 1]) is float: - tokens.insert(index + 1, '*') - continue - return tokens + new_tokens = [] + for index, token in enumerate(tokens): + + if token == '(' and index != 0: + if (type(tokens[index - 1]) is float) or (tokens[index - 1] is ')'): + new_tokens.append('*') + new_tokens.append(token) + else: + new_tokens.append(token) + + elif token == ')' and index != len(tokens) - 1: + if (type(tokens[index + 1]) is float) or (tokens[index+1] in prefix_function): + new_tokens.append(token) + new_tokens.append('*') + else: + new_tokens.append(token) + elif (type(token) is float) and (index != len(tokens)-1): + if (type(tokens[index+1]) is float) or (tokens[index+1] in prefix_function): + new_tokens.append(token) + new_tokens.append('*') + else: + new_tokens.append(token) + else: + new_tokens.append(token) + return new_tokens def verify_to_elements_operator(expression): @@ -134,14 +155,13 @@ def verify_to_elements_operator(expression): ] for el in to_el_operators_separate_whitespace: if el in expression: - raise SpaceIn2ElementOperators('ERROR: {} is wrong operators'.format(el)) + raise ErrorSpaceIn2ElementOperators('ERROR: {} is wrong operators'.format(el)) else: continue def check_space_between_operands(expression=''): """ - get expression and verify it has space between 2 operands :param expression: @@ -153,10 +173,10 @@ def check_space_between_operands(expression=''): try: prev = float(expression[index - 1]) next_ = float(expression[index + 1]) - except Exception: + except ValueError: continue if prev and next_: - raise SpaceBetweenOperands('ERROR: space between operands') + raise ErrorSpaceBetweenOperands('ERROR: space between operands') def brackets_is_balanced(expression): @@ -191,7 +211,7 @@ def calculate_expression(expression): operands = (stack.pop(-1),) stack.append(func(*operands)) - elif len(stack) == 0: + elif not stack: func = unary[token] for i, el in enumerate(expression): if type(el) is not float: @@ -201,25 +221,25 @@ def calculate_expression(expression): res = func(*operands) expression.insert(i, res) else: - func = operators[token][1] + func = operators[token].function operands = (stack.pop(-2), stack.pop(-1)) stack.append(func(*operands)) elif token in prefix_function: func = prefix_function[token] - if func[2] == 1: + if func.args_count == 1: operands = (stack.pop(-1),) - stack.append(func[1](*operands)) - elif func[2] == 2: + stack.append(func.function(*operands)) + elif func.args_count == 2: try: operands = (stack.pop(-2), stack.pop(-1)) - except Exception: + except IndexError: operands = (stack.pop(-1),) - stack.append(func[1](*operands)) + stack.append(func.function(*operands)) if len(stack) == 1: return stack[0] else: - raise WrongOperandsCount('ERROR: Wrong operands count') + raise ErrorWrongOperandsCount('ERROR: Wrong operands count') except Exception as e: return e @@ -291,18 +311,18 @@ def parse_to_reverse_polish_notation(tokens, postfix_expression=None, stack=None tokens.pop(0) break except IndexError: - raise WrongBracketsBalance('ERROR: brackets are not balanced') + raise ErrorWrongBracketsBalance('ERROR: brackets are not balanced') elif token in operators: while True: - if len(stack) is 0: + if not stack: stack.append(tokens.pop(0)) break else: tmp = stack[-1] if tmp in operators: - if (operators[token][0] <= operators[tmp][0]) and (token != '^'): + if (operators[token].priority <= operators[tmp].priority) and (token != '^'): postfix_expression.append(stack.pop()) - elif operators[token][0] > operators[tmp][0]: + elif operators[token].priority > operators[tmp].priority: stack.append(tokens.pop(0)) break else: @@ -324,17 +344,17 @@ def parse_to_reverse_polish_notation(tokens, postfix_expression=None, stack=None if stack[index] != '(' and index != 0: postfix_expression.append(stack.pop()) index -= 1 - if len(stack) is 0: - raise WrongBracketsBalance + if not stack: + raise ErrorWrongBracketsBalance else: stack.pop() break except Exception as e: print(e) else: - raise WrongToken('ERROR: {token} is not correct element of expression'.format(token=token)) - while len(stack): - postfix_expression.append(stack.pop()) + raise ErrorWrongToken('ERROR: {token} is not correct element of expression'.format(token=token)) + + postfix_expression.extend(stack[::-1]) return postfix_expression @@ -345,9 +365,16 @@ def split_by_comparison(expression): :param expression: expression type is string :return: tuple contains parts of expression after splitting and list of comp. operators """ + eq = '==' + ne = '!=' + le = '<=' + ge = '>=' + gt = '>' + lt = '<' expression = re.sub(r'\s', '', expression) - exp = re.split(r'<=|>=|!=|==|<|>', expression) - comparisons_operator = re.findall(r'<=|>=|!=|==|<|>', expression) + exp = re.split(r'{lt}|{ge}|{ne}|{eq}|{lt}|{gt}'.format(le=le, ge=ge, ne=ne, eq=eq, lt=lt, gt=gt), expression) + comparisons_operator = re.findall(r'{lt}|{ge}|{ne}|{eq}|{lt}|{gt}'.format(le=le, ge=ge, ne=ne, + eq=eq, lt=lt, gt=gt), expression) return exp, comparisons_operator @@ -378,10 +405,11 @@ def from_str_to_result(expression): :return: result or raise Exception if expression has error """ if not brackets_is_balanced(expression): - raise WrongBracketsBalance('ERROR: brackets are not balanced') + raise ErrorWrongBracketsBalance('ERROR: brackets are not balanced') expression = get_token(expression) - expression = implicit_mul(expression) + expression = convert_num_and_const_to_float(expression) + expression = check_implicit_mul(expression) expression = parse_to_reverse_polish_notation(expression) try: expression = calculate_expression(expression) diff --git a/final_task/pycalc/test_pycalc.py b/final_task/pycalc/test_pycalc.py index 8a00a5f..d891672 100644 --- a/final_task/pycalc/test_pycalc.py +++ b/final_task/pycalc/test_pycalc.py @@ -18,14 +18,14 @@ # import Exceptions from .pycalc import ( - WrongBracketsBalance, - SpaceBetweenOperands, - SpaceIn2ElementOperators, - WrongToken, + ErrorWrongBracketsBalance, + ErrorSpaceBetweenOperands, + ErrorSpaceIn2ElementOperators, + ErrorWrongToken, ) -class Test(unittest.TestCase): +class TestPyCalc(unittest.TestCase): def setUp(self): self.expression = get_token('(343+pi^3)/(e+sin(3-11))') self.reverse_polish_notation = parse_to_reverse_polish_notation(self.expression) @@ -114,19 +114,19 @@ def test_from_str_to_result(self): self.assertEqual(from_str_to_result('(2.0^(pi/pi+e/e+2.0^0.0))'), 8) def test_error_raising_brackets(self): - with self.assertRaises(WrongBracketsBalance): + with self.assertRaises(ErrorWrongBracketsBalance): from_str_to_result('((1+15)') def test_error_raising_space_between_operands(self): - with self.assertRaises(SpaceBetweenOperands): + with self.assertRaises(ErrorSpaceBetweenOperands): check_space_between_operands("1 + 1 2 3 4 5 6 ") def test_error_raising_space_2el_operators(self): - with self.assertRaises(SpaceIn2ElementOperators): + with self.assertRaises(ErrorSpaceIn2ElementOperators): verify_to_elements_operator('6 < = 6') def test_wrong_token(self): - with self.assertRaises(WrongToken): + with self.assertRaises(ErrorWrongToken): parse_to_reverse_polish_notation(['(', '-1', '+', '5', '+', 'qwe', ')']) def test_zero_divizion(self):