From fc9061ee7027c6717af01f3ee352ae643f38659a Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 8 Nov 2018 23:34:06 +0300 Subject: [PATCH 01/15] Initial GitHub commit --- final_task/pycalc/__init__.py | 0 final_task/pycalc/argparser.py | 35 ++++ final_task/pycalc/calculator.py | 313 +++++++++++++++++++++++++++++ final_task/pycalc/custom_module.py | 7 + final_task/pycalc/pycalc.py | 22 ++ final_task/pycalc/tester.py | 43 ++++ final_task/pycalc/validator.py | 31 +++ final_task/setup.py | 12 ++ 8 files changed, 463 insertions(+) create mode 100644 final_task/pycalc/__init__.py create mode 100755 final_task/pycalc/argparser.py create mode 100755 final_task/pycalc/calculator.py create mode 100644 final_task/pycalc/custom_module.py create mode 100755 final_task/pycalc/pycalc.py create mode 100755 final_task/pycalc/tester.py create mode 100755 final_task/pycalc/validator.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/argparser.py b/final_task/pycalc/argparser.py new file mode 100755 index 0000000..46a75f0 --- /dev/null +++ b/final_task/pycalc/argparser.py @@ -0,0 +1,35 @@ +""" + About +""" + +import argparse + + +class Argparser: + """ + About + """ + + def __init__(self): + """ + About + """ + + self._parser = argparse.ArgumentParser( + description='Pure-python command-line calculator') + + self._parser.add_argument('-m', '--use-modules', metavar='MODULE', + nargs='+', help="additional modules to use", + dest="modules") + + self._parser.add_argument(metavar='EXPRESSION', type=str, nargs=1, + help="expression string to evaluate", + dest="expression") + + def parse_input(self): + """ + About + """ + + args = self._parser.parse_args() + return args diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py new file mode 100755 index 0000000..cfffaa3 --- /dev/null +++ b/final_task/pycalc/calculator.py @@ -0,0 +1,313 @@ +"""Main module of the calculator.""" + +import math +import re + + +class Calculator: + """Handles all operations related to the conversion and evaluation + of expressions.""" + + BINARIES = ( + ('^', lambda a, b: a ** b), # ** + ('/', lambda a, b: a / b), + ('//', lambda a, b: a // b), + ('*', lambda a, b: a * b), + ('%', lambda a, b: a % b), + ('+', lambda a, b: a + b), + # ('-', lambda a, b: a - b), + ('<=', lambda a, b: a <= b), + ('>=', lambda a, b: a >= b), + ('<', lambda a, b: a < b), + ('>', lambda a, b: a > b), + ('!=', lambda a, b: a != b), + ('==', lambda a, b: a == b) + ) + + FUNCTIONS = ( + ('pow', lambda a, b: pow(a, b)), + ('abs', lambda a: abs(a)), + ('round', lambda a: round(a)), + ('ctan', lambda a: 1 / math.tan(a)) + ) + + def __init__(self, validator): + self._validator = validator + self._modules = [math] + + def calc_start(self, expression, modules=None): + """Entry point of calculating. Validates, transforms and finally + calculates given expression.""" + + self._validator.validate(expression) + expression = self.transform(expression) + + if modules is not None: + for m in modules: + new_module = __import__(m) + self._modules.append(new_module) + + expression = self.replace_constants(expression) + expression = self.calculate_functions(expression) + expression = self.handle_implicit_multiplication(expression) + + result = self.calculate(expression) + return self.convert(result) + + def transform(self, expression): + """Transforms the expression into Calculator friendly form.""" + + expression = expression.lower() + expression = expression.strip("'") + expression = expression.strip('"') + expression = expression.replace(' ', '') + expression = expression.replace('**', '^') + expression = self.handle_subtraction(expression) + + return expression + + def find_left_num(self, expression, sign_pos): + """Returns a number that lays left from the sign_pos (or + None if it doesn't exist.""" + + pattern = r'([0-9\[\]\.\-]|(e\+)|(e\-))+$' + num = re.search(pattern, expression[:sign_pos]) + return num.group(0) + + def find_right_num(self, expression, sign_pos): + """Returns a number that lays right from the sign_pos (or + None if it doesn't exist.""" + + pattern = r'^([0-9\[\]\.\-]|(e\+)|(e\-))+' + num = re.search(pattern, expression[sign_pos+1:]) + return num.group(0) + + def calculate(self, expression=None): + """Recursive function that divides the expression, calculates its + right and left parts and whole result.""" + + expression = self.calculate_nested(expression) + expression = self.handle_extra_signs(expression) + + for sign, func in self.BINARIES: + while True: + + spos = None + if sign == '^': + spos = expression.rfind(sign) + elif sign == '+': + spos = expression.find(sign) + while spos != -1 and expression[spos-1] == 'e': + spos = expression[spos+2:].find(sign) + else: + spos = expression.find(sign) + + if spos == -1: + break + + if sign == '^': + left = self.find_left_num(expression, spos) + if left.find(']') == -1: + left = left.replace('-', '') + else: + left = self.find_left_num(expression, spos) + + slen = len(sign) - 1 + right = self.find_right_num(expression, spos + slen) + + result = self.calculate_elementary( + expression[spos:spos + slen + 1], left, right) + expression = expression.replace( + left + sign + right, str(result), 1) + + return expression.strip('[]') + + def calculate_functions(self, expression): + """Calculates all founded functions and returns an expression that + contains only elementary binary operations.""" + + # List reversion here makes it possible to calculate nested functions + pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' + func_name_list = re.findall(pattern, expression)[::-1] + + for func_name in func_name_list: + if func_name in ('False', 'True'): + continue + + func, is_callable = self.get_func_or_const_by_name(func_name) + if func is None: + raise TypeError("no such function " + func_name) + + if is_callable is False: + continue + + fpos = expression.rfind(func_name) + args, arg_end = self.get_func_args(expression, func_name, fpos) + + if args is not None: + converted_args = self.convert_arguments(args) + result = func(*converted_args) + else: + result = func() + + expression = expression.replace( + expression[fpos:arg_end], '(' + str(result) + ')', 1 + ) + + return expression + + def get_func_args(self, expression, func_name, func_pos): + """Finds all the arguments of the function, located on the func_pos.""" + + arg_start = func_pos + len(func_name) + arg_end = arg_start + expression[arg_start:].find(')') + arguments = expression[arg_start:arg_end] + + while arguments.count('(') != arguments.count(')'): + arg_end += 1 + expression[arg_end:].find(')') + arguments = expression[arg_start:arg_end] + + argument_list = arguments[1:-1].split(',') + if '' in argument_list: + argument_list = None + + return argument_list, arg_end + + def replace_constants(self, expression): + """Finds constants in imported and user modules and builtins.""" + + pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' + names = re.findall(pattern, expression) + + for n in names: + obj, is_callable = self.get_func_or_const_by_name(n) + if is_callable or obj is None: + continue + + # Parentheses are used to prevent mixing numbers + # with replaced constants. + expression = expression.replace(n, '(' + str(obj) + ')') + + return expression + + def get_func_or_const_by_name(self, requested_name): + """Finds by name and returns a function if it exists, else + returns None.""" + + result = None, None + for fname, obj in self.FUNCTIONS: + if fname == requested_name: + result = obj, True + + for m in self._modules: + if hasattr(m, requested_name): + obj = getattr(m, requested_name) + if callable(obj): + result = obj, True + else: + result = obj, False + + return result + + def convert(self, a): + """Converts an argument to int, bool or float.""" + + if not isinstance(a, str): + return a + + if a in ('True', 'False'): + a = True if a == 'True' else False + return a + + try: + a = int(a) + except ValueError: + a = float(a) + + return a + + def convert_arguments(self, args): + """Returns a list of converted arguments.""" + + converted_args = [] + for a in args: + result_a = self.calculate(a) + converted_args.append(self.convert(result_a)) + + return converted_args + + def handle_subtraction(self, expression): + """Modifies subtractions in given expression to make them + calculator friendly.""" + + pattern = r'[0-9\]]\-' + cases = re.findall(pattern, expression) + for c in cases: + expression = expression.replace(c, c[0] + '+' + c[1]) + + return expression + + def handle_implicit_multiplication(self, expression): + """Replaces all implicit multiplication cases with obvious ones.""" + + patterns = (r'[0-9][A-Za-z][^\-+]', r'\)[0-9]', r'[0-9]\(', r'\)\(') + for p in patterns: + cases = re.findall(p, expression) + for c in cases: + expression = expression.replace(c, c[0] + '*' + c[1]) + + return expression + + def handle_extra_signs(self, expression): + """Gets rid of extra pluses and minuses in given expression.""" + + pattern = r'[-+]{2,}' + cases = re.findall(pattern, expression) + for c in cases: + minus_count = c.count('-') + if minus_count % 2 == 0: + expression = expression.replace(c, '+', 1) + else: + expression = expression.replace(c, '-', 1) + + expression = self.handle_subtraction(expression) + return expression + + def calculate_elementary(self, operation, *args): + """Calculates elementary binary operations like addition, subtraction, + multiplication, division etc.""" + + result = None + + args = list(re.sub(r'[\[\]]', '', a) for a in args) + args = list(self.handle_extra_signs(a) for a in args) + converted_args = list(self.convert(a) for a in args) + + self._validator.check(operation, *converted_args) + for o, func in self.BINARIES: + if o == operation: + result = func(*converted_args) + break + + return result + + def calculate_nested(self, expression): + """Returns the result of nested expression calculating.""" + + while True: + nested = self.get_nested(expression) + if nested is None: + break + nested_result = self.calculate(nested[1:-1]) + expression = expression.replace( + nested, '[' + str(nested_result) + ']') + + return expression + + def get_nested(self, expression): + """Finds and returns nested expression (with no nested inside it) if + it exists, else returns None.""" + + # From '(' to ')' blocking any parentheses inside + nested = re.search(r'\([^()]*\)', expression) + return nested.group(0) if nested is not None else None diff --git a/final_task/pycalc/custom_module.py b/final_task/pycalc/custom_module.py new file mode 100644 index 0000000..b33e567 --- /dev/null +++ b/final_task/pycalc/custom_module.py @@ -0,0 +1,7 @@ +""" + About. +""" + + +def cube_volume(arg1): + return arg1*arg1*arg1 diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py new file mode 100755 index 0000000..f736db4 --- /dev/null +++ b/final_task/pycalc/pycalc.py @@ -0,0 +1,22 @@ +"""Entry point of pycalc project.""" + +from argparser import Argparser +from calculator import Calculator +from validator import Validator + + +def main(): + argparser = Argparser() + validator = Validator() + calc = Calculator(validator) + + args = argparser.parse_input() + expression = args.expression[0] + modules = args.modules + result = calc.calc_start(expression, modules) + + print(result) # :) + + +if __name__ == "__main__": + main() diff --git a/final_task/pycalc/tester.py b/final_task/pycalc/tester.py new file mode 100755 index 0000000..2b6dfc7 --- /dev/null +++ b/final_task/pycalc/tester.py @@ -0,0 +1,43 @@ +"""Unittests for calculator.""" + +import unittest +import validator +import calculator + + +class CalculatorTester(unittest.TestCase): + """Handles all testing operation.""" + + def test_calculate(self): + val = validator.Validator() + calc = calculator.Calculator(val) + + self.assertEqual(calc.calc_start('2'), 2) + self.assertEqual(calc.calc_start('-2'), -2) + + self.assertEqual(calc.calc_start('2+2'), 4) + self.assertEqual(calc.calc_start('2-2'), 0) + self.assertEqual(calc.calc_start('2*2'), 4) + self.assertEqual(calc.calc_start('2/2'), 1.0) + self.assertEqual(calc.calc_start('15%2'), 1) + self.assertEqual(calc.calc_start('2**16'), 65536) + + self.assertEqual(calc.calc_start('2*log10(100)*3'), 12.0) + self.assertEqual(calc.calc_start('pow(2,2)sqrt(625)'), 100.0) + self.assertEqual(calc.calc_start('pow(2, 256)'), 2 ** 256) + + self.assertEqual(calc.calc_start('2+2*2'), 6) + self.assertEqual(calc.calc_start('2/2*2'), 2) + self.assertEqual(calc.calc_start('2*2/2'), 2) + + self.assertEqual(calc.calc_start('5*(5+5)'), 50) + self.assertEqual(calc.calc_start('5-(5+5)'), -5) + self.assertEqual(calc.calc_start('5(5+5)5'), 250) + self.assertEqual(calc.calc_start('2(2+2)(2+2)2'), 64) + self.assertEqual(calc.calc_start('2(2((2+2)2))2'), 64) + + self.assertEqual(calc.calc_start( + '(10-20)(20+10)log10(100)(sqrt(25)+5)'), -6000) + self.assertEqual(calc.calc_start('32-32*2+50-5**2*2'), -32) + self.assertEqual(calc.calc_start( + '9*(2+150*(650/5)-190+445/(20-25))+12+90/5-173036/2*2'), 1.0) diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py new file mode 100755 index 0000000..256e95e --- /dev/null +++ b/final_task/pycalc/validator.py @@ -0,0 +1,31 @@ +"""Tools for validating given expressions.""" + +import re + + +class Validator: + """Validator checks given expression for possible errors.""" + + def validate(self, expression): + """Fully validates given expression for user error""" + + if expression.count('(') != expression.count(')'): + raise ValueError("brackets are not balanced") + + pattern = r'[0-9]+\s+[0-9]+' + if re.search(pattern, expression) is not None: + raise ValueError("ambiguous spaces between numbers") + + pattern = r'[<>=*\/]\s+[<>=*\/]' + if re.search(pattern, expression) is not None: + raise ValueError("ambiguous spaces between signs") + + def check(self, sign, left, right): + """Rapidly checks mathematical errors.""" + + if left is None or right is None: + raise ValueError("please, check your expression") + + if sign == '^' and left < 0 and isinstance(right, float): + error = "negative number cannot be raised to a fractional power" + raise ValueError(error) diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..8f0c708 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages + +setup( + name="Pycalc project", + version='1.0', + author="Alexander Gutyra", + author_email="gutyra13@gmail.com" + description="Simple pure-Python calculator with custom modules support." + packages=find_packages(), + entry_points={ + 'console_scripts': ['pycalc = pycalc.__main__:main'] + } From 85ea04bab1f851dff77661adee0b71a559cea026 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 8 Nov 2018 23:43:49 +0300 Subject: [PATCH 02/15] Fixed setup.py --- final_task/setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/final_task/setup.py b/final_task/setup.py index 8f0c708..7ec5a3c 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -4,9 +4,10 @@ name="Pycalc project", version='1.0', author="Alexander Gutyra", - author_email="gutyra13@gmail.com" - description="Simple pure-Python calculator with custom modules support." + author_email="gutyra13@gmail.com", + description="Simple pure-Python calculator with custom modules support.", packages=find_packages(), entry_points={ 'console_scripts': ['pycalc = pycalc.__main__:main'] } +) From 0630347580e8d4c1b3d0919de51f142b6e425667 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 8 Nov 2018 23:48:46 +0300 Subject: [PATCH 03/15] Amended setup.py --- 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 7ec5a3c..fda5593 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -8,6 +8,6 @@ description="Simple pure-Python calculator with custom modules support.", packages=find_packages(), entry_points={ - 'console_scripts': ['pycalc = pycalc.__main__:main'] + 'console_scripts': ['pycalc = pycalc:main'] } ) From af2027e755e160a5ef0e5bb602cbb667b084d211 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Fri, 9 Nov 2018 00:00:47 +0300 Subject: [PATCH 04/15] Amended setup.py: fixed entry point --- 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 fda5593..6118f8b 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -8,6 +8,6 @@ description="Simple pure-Python calculator with custom modules support.", packages=find_packages(), entry_points={ - 'console_scripts': ['pycalc = pycalc:main'] + 'console_scripts': ['pycalc = pycalc.pycalc'] } ) From b14c3240695880a9616bd06cf8699a50dbccaa97 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Fri, 9 Nov 2018 00:06:28 +0300 Subject: [PATCH 05/15] Fixed ImportError --- final_task/pycalc/pycalc.py | 6 +++--- final_task/pycalc/tester.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index f736db4..147ee26 100755 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -1,8 +1,8 @@ """Entry point of pycalc project.""" -from argparser import Argparser -from calculator import Calculator -from validator import Validator +from .argparser import Argparser +from .calculator import Calculator +from .validator import Validator def main(): diff --git a/final_task/pycalc/tester.py b/final_task/pycalc/tester.py index 2b6dfc7..2d2957d 100755 --- a/final_task/pycalc/tester.py +++ b/final_task/pycalc/tester.py @@ -1,16 +1,16 @@ """Unittests for calculator.""" import unittest -import validator -import calculator +from .validator import Validator +from .calculator import Calculator class CalculatorTester(unittest.TestCase): """Handles all testing operation.""" def test_calculate(self): - val = validator.Validator() - calc = calculator.Calculator(val) + val = Validator() + calc = Calculator(val) self.assertEqual(calc.calc_start('2'), 2) self.assertEqual(calc.calc_start('-2'), -2) From 96fdc333e61be3a474efb68ab3f7532291857b68 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Fri, 9 Nov 2018 00:14:55 +0300 Subject: [PATCH 06/15] Fixed TypeError --- 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 6118f8b..fa39c3b 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -8,6 +8,6 @@ description="Simple pure-Python calculator with custom modules support.", packages=find_packages(), entry_points={ - 'console_scripts': ['pycalc = pycalc.pycalc'] + 'console_scripts': ['pycalc = pycalc.pycalc.main'] } ) From 47c9e2b645b5fba8deb7b3f230824e9373c6bcde Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Fri, 9 Nov 2018 10:51:13 +0300 Subject: [PATCH 07/15] Fixed entry point --- 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 fa39c3b..eb0f8eb 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -8,6 +8,6 @@ description="Simple pure-Python calculator with custom modules support.", packages=find_packages(), entry_points={ - 'console_scripts': ['pycalc = pycalc.pycalc.main'] + 'console_scripts': ['pycalc = pycalc.pycalc:main'] } ) From 2496efd1bd1a88f3368d14274b68bdb2193464a7 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Fri, 9 Nov 2018 13:37:22 +0300 Subject: [PATCH 08/15] Added correct error handing --- final_task/pycalc/argparser.py | 16 ++---------- final_task/pycalc/calculator.py | 40 ++++++++++++++++++++++-------- final_task/pycalc/custom_module.py | 4 +-- final_task/pycalc/pycalc.py | 2 +- final_task/pycalc/validator.py | 20 ++++++++++----- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/final_task/pycalc/argparser.py b/final_task/pycalc/argparser.py index 46a75f0..500f549 100755 --- a/final_task/pycalc/argparser.py +++ b/final_task/pycalc/argparser.py @@ -1,20 +1,12 @@ -""" - About -""" +"""Handles all console input operations.""" import argparse class Argparser: - """ - About - """ + """Provides correct data console input""" def __init__(self): - """ - About - """ - self._parser = argparse.ArgumentParser( description='Pure-python command-line calculator') @@ -27,9 +19,5 @@ def __init__(self): dest="expression") def parse_input(self): - """ - About - """ - args = self._parser.parse_args() return args diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index cfffaa3..0559b34 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -10,8 +10,8 @@ class Calculator: BINARIES = ( ('^', lambda a, b: a ** b), # ** - ('/', lambda a, b: a / b), ('//', lambda a, b: a // b), + ('/', lambda a, b: a / b), ('*', lambda a, b: a * b), ('%', lambda a, b: a % b), ('+', lambda a, b: a + b), @@ -72,6 +72,9 @@ def find_left_num(self, expression, sign_pos): pattern = r'([0-9\[\]\.\-]|(e\+)|(e\-))+$' num = re.search(pattern, expression[:sign_pos]) + if num is None: + print("ERROR: please, check your expression.") + exit(1) return num.group(0) def find_right_num(self, expression, sign_pos): @@ -79,7 +82,10 @@ def find_right_num(self, expression, sign_pos): None if it doesn't exist.""" pattern = r'^([0-9\[\]\.\-]|(e\+)|(e\-))+' - num = re.search(pattern, expression[sign_pos+1:]) + num = re.search(pattern, expression[sign_pos + 1:]) + if num is None: + print("ERROR: please, check your expression.") + exit(1) return num.group(0) def calculate(self, expression=None): @@ -97,8 +103,8 @@ def calculate(self, expression=None): spos = expression.rfind(sign) elif sign == '+': spos = expression.find(sign) - while spos != -1 and expression[spos-1] == 'e': - spos = expression[spos+2:].find(sign) + while spos != -1 and expression[spos - 1] == 'e': + spos = expression[spos + 2:].find(sign) else: spos = expression.find(sign) @@ -136,7 +142,8 @@ def calculate_functions(self, expression): func, is_callable = self.get_func_or_const_by_name(func_name) if func is None: - raise TypeError("no such function " + func_name) + print("ERROR: no such function " + func_name + ".") + exit(1) if is_callable is False: continue @@ -144,11 +151,16 @@ def calculate_functions(self, expression): fpos = expression.rfind(func_name) args, arg_end = self.get_func_args(expression, func_name, fpos) - if args is not None: - converted_args = self.convert_arguments(args) - result = func(*converted_args) - else: - result = func() + result = '' + try: + if args is not None: + converted_args = self.convert_arguments(args) + result = func(*converted_args) + else: + result = func() + except TypeError: + print("ERROR: please, check function " + func_name + ".") + exit(1) expression = expression.replace( expression[fpos:arg_end], '(' + str(result) + ')', 1 @@ -281,7 +293,13 @@ def calculate_elementary(self, operation, *args): args = list(re.sub(r'[\[\]]', '', a) for a in args) args = list(self.handle_extra_signs(a) for a in args) - converted_args = list(self.convert(a) for a in args) + + converted_args = [] + try: + converted_args = list(self.convert(a) for a in args) + except ValueError: + print("ERROR: please, check your expression.") + exit(1) self._validator.check(operation, *converted_args) for o, func in self.BINARIES: diff --git a/final_task/pycalc/custom_module.py b/final_task/pycalc/custom_module.py index b33e567..7447c15 100644 --- a/final_task/pycalc/custom_module.py +++ b/final_task/pycalc/custom_module.py @@ -1,6 +1,4 @@ -""" - About. -""" +"""Custom module is created to test custom function support feature.""" def cube_volume(arg1): diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index 147ee26..f30253b 100755 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -15,7 +15,7 @@ def main(): modules = args.modules result = calc.calc_start(expression, modules) - print(result) # :) + print(result) if __name__ == "__main__": diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py index 256e95e..06be063 100755 --- a/final_task/pycalc/validator.py +++ b/final_task/pycalc/validator.py @@ -9,23 +9,31 @@ class Validator: def validate(self, expression): """Fully validates given expression for user error""" + if len(expression) == 0: + print("ERROR: cannot calculate empty expression.") + exit(1) + if expression.count('(') != expression.count(')'): - raise ValueError("brackets are not balanced") + print("ERROR: brackets are not balanced.") + exit(1) pattern = r'[0-9]+\s+[0-9]+' if re.search(pattern, expression) is not None: - raise ValueError("ambiguous spaces between numbers") + print("ERROR: ambiguous spaces between numbers.") + exit(1) pattern = r'[<>=*\/]\s+[<>=*\/]' if re.search(pattern, expression) is not None: - raise ValueError("ambiguous spaces between signs") + print("ERROR: ambiguous spaces between signs.") + exit(1) def check(self, sign, left, right): """Rapidly checks mathematical errors.""" if left is None or right is None: - raise ValueError("please, check your expression") + print("ERROR: please, check your expression.") + exit(1) if sign == '^' and left < 0 and isinstance(right, float): - error = "negative number cannot be raised to a fractional power" - raise ValueError(error) + print("negative number cannot be raised to a fractional power.") + exit(1) From 16b368b8a76c3d2b283caeca0a593f044ea79672 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 15 Nov 2018 13:41:03 +0300 Subject: [PATCH 09/15] Moved error handling to validator, refactored calculate() function --- final_task/pycalc/calculator.py | 79 ++++++++++++++++++--------------- final_task/pycalc/validator.py | 24 +++++----- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index 0559b34..9297a5e 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -1,4 +1,5 @@ """Main module of the calculator.""" +# TODO: set custom module's higher priority than included (math) import math import re @@ -15,7 +16,6 @@ class Calculator: ('*', lambda a, b: a * b), ('%', lambda a, b: a % b), ('+', lambda a, b: a + b), - # ('-', lambda a, b: a - b), ('<=', lambda a, b: a <= b), ('>=', lambda a, b: a >= b), ('<', lambda a, b: a < b), @@ -25,7 +25,7 @@ class Calculator: ) FUNCTIONS = ( - ('pow', lambda a, b: pow(a, b)), + ('pow', lambda a, b, c: pow(a, b)), ('abs', lambda a: abs(a)), ('round', lambda a: round(a)), ('ctan', lambda a: 1 / math.tan(a)) @@ -37,16 +37,11 @@ def __init__(self, validator): def calc_start(self, expression, modules=None): """Entry point of calculating. Validates, transforms and finally - calculates given expression.""" + calculates given expression. Returns calculating result.""" self._validator.validate(expression) + self.import_modules(modules) expression = self.transform(expression) - - if modules is not None: - for m in modules: - new_module = __import__(m) - self._modules.append(new_module) - expression = self.replace_constants(expression) expression = self.calculate_functions(expression) expression = self.handle_implicit_multiplication(expression) @@ -54,6 +49,14 @@ def calc_start(self, expression, modules=None): result = self.calculate(expression) return self.convert(result) + def import_modules(self, modules): + """Imports all modules from given list.""" + + if modules is not None: + for m in modules: + new_module = __import__(m) + self._modules.append(new_module) + def transform(self, expression): """Transforms the expression into Calculator friendly form.""" @@ -73,8 +76,8 @@ def find_left_num(self, expression, sign_pos): pattern = r'([0-9\[\]\.\-]|(e\+)|(e\-))+$' num = re.search(pattern, expression[:sign_pos]) if num is None: - print("ERROR: please, check your expression.") - exit(1) + self._validator.assert_error( + "ERROR: please, check your expression.") return num.group(0) def find_right_num(self, expression, sign_pos): @@ -84,8 +87,8 @@ def find_right_num(self, expression, sign_pos): pattern = r'^([0-9\[\]\.\-]|(e\+)|(e\-))+' num = re.search(pattern, expression[sign_pos + 1:]) if num is None: - print("ERROR: please, check your expression.") - exit(1) + self._validator.assert_error( + "ERROR: please, check your expression.") return num.group(0) def calculate(self, expression=None): @@ -97,37 +100,43 @@ def calculate(self, expression=None): for sign, func in self.BINARIES: while True: - - spos = None - if sign == '^': - spos = expression.rfind(sign) - elif sign == '+': - spos = expression.find(sign) - while spos != -1 and expression[spos - 1] == 'e': - spos = expression[spos + 2:].find(sign) - else: - spos = expression.find(sign) - - if spos == -1: + sign_pos = self.find_sign(expression, sign) + if sign_pos == -1: break if sign == '^': - left = self.find_left_num(expression, spos) + left = self.find_left_num(expression, sign_pos) if left.find(']') == -1: left = left.replace('-', '') else: - left = self.find_left_num(expression, spos) + left = self.find_left_num(expression, sign_pos) slen = len(sign) - 1 - right = self.find_right_num(expression, spos + slen) + right = self.find_right_num(expression, sign_pos + slen) result = self.calculate_elementary( - expression[spos:spos + slen + 1], left, right) + expression[sign_pos:sign_pos + slen + 1], left, right) expression = expression.replace( left + sign + right, str(result), 1) return expression.strip('[]') + def find_sign(self, expression, sign): + """Returns a position of given sign in the + expression (-1 if not found).""" + + sign_pos = None + if sign == '^': + sign_pos = expression.rfind(sign) + elif sign == '+': + sign_pos = expression.find(sign) + while sign_pos != -1 and expression[sign_pos - 1] == 'e': + sign_pos = expression[sign_pos + 2:].find(sign) + else: + sign_pos = expression.find(sign) + + return sign_pos + def calculate_functions(self, expression): """Calculates all founded functions and returns an expression that contains only elementary binary operations.""" @@ -142,8 +151,8 @@ def calculate_functions(self, expression): func, is_callable = self.get_func_or_const_by_name(func_name) if func is None: - print("ERROR: no such function " + func_name + ".") - exit(1) + self._validator.assert_error( + "ERROR: no such function " + func_name + ".") if is_callable is False: continue @@ -159,8 +168,8 @@ def calculate_functions(self, expression): else: result = func() except TypeError: - print("ERROR: please, check function " + func_name + ".") - exit(1) + self._validator.assert_error( + "ERROR: please, check function " + func_name + ".") expression = expression.replace( expression[fpos:arg_end], '(' + str(result) + ')', 1 @@ -298,8 +307,8 @@ def calculate_elementary(self, operation, *args): try: converted_args = list(self.convert(a) for a in args) except ValueError: - print("ERROR: please, check your expression.") - exit(1) + self._validator.assert_error( + "ERROR: please, check your expression.") self._validator.check(operation, *converted_args) for o, func in self.BINARIES: diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py index 06be063..f355d66 100755 --- a/final_task/pycalc/validator.py +++ b/final_task/pycalc/validator.py @@ -10,30 +10,30 @@ def validate(self, expression): """Fully validates given expression for user error""" if len(expression) == 0: - print("ERROR: cannot calculate empty expression.") - exit(1) + self.assert_error("ERROR: cannot calculate empty expression.") if expression.count('(') != expression.count(')'): - print("ERROR: brackets are not balanced.") - exit(1) + self.assert_error("ERROR: brackets are not balanced.") pattern = r'[0-9]+\s+[0-9]+' if re.search(pattern, expression) is not None: - print("ERROR: ambiguous spaces between numbers.") - exit(1) + self.assert_error("ERROR: ambiguous spaces between numbers.") pattern = r'[<>=*\/]\s+[<>=*\/]' if re.search(pattern, expression) is not None: - print("ERROR: ambiguous spaces between signs.") - exit(1) + self.assert_error("ERROR: ambiguous spaces between signs.") def check(self, sign, left, right): """Rapidly checks mathematical errors.""" if left is None or right is None: - print("ERROR: please, check your expression.") - exit(1) + self.assert_error("ERROR: please, check your expression.") if sign == '^' and left < 0 and isinstance(right, float): - print("negative number cannot be raised to a fractional power.") - exit(1) + self.assert_error( + "ERROR: negative number cannot be raised to fractional power." + ) + + def assert_error(self, error_text, exitcode=1): + print(error_text) + exit(exitcode) From d16cea289c927d5e821e7596a267f9fd7f00da15 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 15 Nov 2018 19:54:42 +0300 Subject: [PATCH 10/15] Improved constant replacement: now it can solve ambiguous situations like --- final_task/pycalc/calculator.py | 105 ++++++++++++++++++----------- final_task/pycalc/custom_module.py | 2 + final_task/pycalc/validator.py | 14 ++-- 3 files changed, 74 insertions(+), 47 deletions(-) diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index 9297a5e..66258c6 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -1,5 +1,4 @@ """Main module of the calculator.""" -# TODO: set custom module's higher priority than included (math) import math import re @@ -41,10 +40,10 @@ def calc_start(self, expression, modules=None): self._validator.validate(expression) self.import_modules(modules) + expression = self.transform(expression) expression = self.replace_constants(expression) expression = self.calculate_functions(expression) - expression = self.handle_implicit_multiplication(expression) result = self.calculate(expression) return self.convert(result) @@ -76,8 +75,7 @@ def find_left_num(self, expression, sign_pos): pattern = r'([0-9\[\]\.\-]|(e\+)|(e\-))+$' num = re.search(pattern, expression[:sign_pos]) if num is None: - self._validator.assert_error( - "ERROR: please, check your expression.") + self._validator.assert_error("please, check your expression.") return num.group(0) def find_right_num(self, expression, sign_pos): @@ -87,8 +85,7 @@ def find_right_num(self, expression, sign_pos): pattern = r'^([0-9\[\]\.\-]|(e\+)|(e\-))+' num = re.search(pattern, expression[sign_pos + 1:]) if num is None: - self._validator.assert_error( - "ERROR: please, check your expression.") + self._validator.assert_error("please, check your expression.") return num.group(0) def calculate(self, expression=None): @@ -96,6 +93,7 @@ def calculate(self, expression=None): right and left parts and whole result.""" expression = self.calculate_nested(expression) + expression = self.handle_implicit_multiplication(expression) expression = self.handle_extra_signs(expression) for sign, func in self.BINARIES: @@ -149,13 +147,13 @@ def calculate_functions(self, expression): if func_name in ('False', 'True'): continue - func, is_callable = self.get_func_or_const_by_name(func_name) - if func is None: + functions = self.get_reserved(c=True) + func = None + if func_name in functions: + func = self.get_reserved_by_name(func_name) + else: self._validator.assert_error( - "ERROR: no such function " + func_name + ".") - - if is_callable is False: - continue + "no such function " + func_name + ".") fpos = expression.rfind(func_name) args, arg_end = self.get_func_args(expression, func_name, fpos) @@ -169,7 +167,7 @@ def calculate_functions(self, expression): result = func() except TypeError: self._validator.assert_error( - "ERROR: please, check function " + func_name + ".") + "please, check function " + func_name + ".") expression = expression.replace( expression[fpos:arg_end], '(' + str(result) + ')', 1 @@ -178,7 +176,8 @@ def calculate_functions(self, expression): return expression def get_func_args(self, expression, func_name, func_pos): - """Finds all the arguments of the function, located on the func_pos.""" + """Finds all the arguments of the function, + located on the func_pos (including nested ones).""" arg_start = func_pos + len(func_name) arg_end = arg_start + expression[arg_start:].find(')') @@ -194,41 +193,65 @@ def get_func_args(self, expression, func_name, func_pos): return argument_list, arg_end + def get_reserved(self, c=False): + """Returns a list of all the constants found in imported modules.""" + + result = [] + for m in self._modules: + for d in dir(m): + obj = getattr(m, d) + if callable(obj) is c and not d.startswith('_'): + result.append(d) + + if c: + for func_name, _ in self.FUNCTIONS: + result.append(func_name) + + # Sort here is used to prevent replacing + # letters of long reserved names + result.sort(key=lambda a: len(a), reverse=True) + return result + def replace_constants(self, expression): """Finds constants in imported and user modules and builtins.""" + const_pattern = '|'.join(self.get_reserved(c=False)) + func_pattern = '|'.join(self.get_reserved(c=True)) pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' - names = re.findall(pattern, expression) - for n in names: - obj, is_callable = self.get_func_or_const_by_name(n) - if is_callable or obj is None: - continue + cases = re.finditer(pattern, expression) + for case in cases: + c_str = case.group() + c_pos = case.start() - # Parentheses are used to prevent mixing numbers - # with replaced constants. - expression = expression.replace(n, '(' + str(obj) + ')') + # Upper is used to prevent replacing + # letters in functions (e. g. "eexp(e)") + replaced = c_str + funcs = re.findall(func_pattern, replaced) + for f in funcs: + replaced = re.sub(f, f.upper(), replaced) - return expression + constants = re.findall(const_pattern, c_str) + for const in constants: + obj = self.get_reserved_by_name(const) + replaced = replaced.replace(const, '(' + str(obj) + ')') - def get_func_or_const_by_name(self, requested_name): - """Finds by name and returns a function if it exists, else - returns None.""" + expression = expression[:c_pos] \ + + expression[c_pos:].replace(c_str, replaced, 1) - result = None, None - for fname, obj in self.FUNCTIONS: - if fname == requested_name: - result = obj, True + return expression.lower() + + def get_reserved_by_name(self, requested_name): + """Finds a function or constant by name.""" for m in self._modules: if hasattr(m, requested_name): obj = getattr(m, requested_name) - if callable(obj): - result = obj, True - else: - result = obj, False + return obj - return result + for func_name, func in self.FUNCTIONS: + if func_name == requested_name: + return func def convert(self, a): """Converts an argument to int, bool or float.""" @@ -252,7 +275,7 @@ def convert_arguments(self, args): converted_args = [] for a in args: - result_a = self.calculate(a) + result_a = self.calc_start(a) converted_args.append(self.convert(result_a)) return converted_args @@ -271,7 +294,10 @@ def handle_subtraction(self, expression): def handle_implicit_multiplication(self, expression): """Replaces all implicit multiplication cases with obvious ones.""" - patterns = (r'[0-9][A-Za-z][^\-+]', r'\)[0-9]', r'[0-9]\(', r'\)\(') + patterns = ( + r'[0-9][A-Za-z][^\-+]', r'[\)\]][0-9]', + r'[0-9][\[\(]', r'\)\(|\]\[' + ) for p in patterns: cases = re.findall(p, expression) for c in cases: @@ -307,8 +333,7 @@ def calculate_elementary(self, operation, *args): try: converted_args = list(self.convert(a) for a in args) except ValueError: - self._validator.assert_error( - "ERROR: please, check your expression.") + self._validator.assert_error("please, check your expression.") self._validator.check(operation, *converted_args) for o, func in self.BINARIES: @@ -327,7 +352,7 @@ def calculate_nested(self, expression): break nested_result = self.calculate(nested[1:-1]) expression = expression.replace( - nested, '[' + str(nested_result) + ']') + nested, '[' + str(nested_result) + ']', 1) return expression diff --git a/final_task/pycalc/custom_module.py b/final_task/pycalc/custom_module.py index 7447c15..0d1e6e1 100644 --- a/final_task/pycalc/custom_module.py +++ b/final_task/pycalc/custom_module.py @@ -1,5 +1,7 @@ """Custom module is created to test custom function support feature.""" +my_const = 123.456 + def cube_volume(arg1): return arg1*arg1*arg1 diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py index f355d66..d7d80f7 100755 --- a/final_task/pycalc/validator.py +++ b/final_task/pycalc/validator.py @@ -10,30 +10,30 @@ def validate(self, expression): """Fully validates given expression for user error""" if len(expression) == 0: - self.assert_error("ERROR: cannot calculate empty expression.") + self.assert_error("cannot calculate empty expression.") if expression.count('(') != expression.count(')'): - self.assert_error("ERROR: brackets are not balanced.") + self.assert_error("brackets are not balanced.") pattern = r'[0-9]+\s+[0-9]+' if re.search(pattern, expression) is not None: - self.assert_error("ERROR: ambiguous spaces between numbers.") + self.assert_error("ambiguous spaces between numbers.") pattern = r'[<>=*\/]\s+[<>=*\/]' if re.search(pattern, expression) is not None: - self.assert_error("ERROR: ambiguous spaces between signs.") + self.assert_error("ambiguous spaces between signs.") def check(self, sign, left, right): """Rapidly checks mathematical errors.""" if left is None or right is None: - self.assert_error("ERROR: please, check your expression.") + self.assert_error("please, check your expression.") if sign == '^' and left < 0 and isinstance(right, float): self.assert_error( - "ERROR: negative number cannot be raised to fractional power." + "negative number cannot be raised to fractional power." ) def assert_error(self, error_text, exitcode=1): - print(error_text) + print("ERROR: " + error_text) exit(exitcode) From 00c616fb451a83403f2212b19aabf2b6243ff50a Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 15 Nov 2018 20:15:17 +0300 Subject: [PATCH 11/15] Made some utility methods static --- final_task/pycalc/calculator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index 66258c6..b2a94f2 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -253,7 +253,8 @@ def get_reserved_by_name(self, requested_name): if func_name == requested_name: return func - def convert(self, a): + @staticmethod + def convert(a): """Converts an argument to int, bool or float.""" if not isinstance(a, str): @@ -356,7 +357,8 @@ def calculate_nested(self, expression): return expression - def get_nested(self, expression): + @staticmethod + def get_nested(expression): """Finds and returns nested expression (with no nested inside it) if it exists, else returns None.""" From f82422ac3bed6fb1f6a7f3df4ec7bb01eab90bc5 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Thu, 15 Nov 2018 23:50:38 +0300 Subject: [PATCH 12/15] Added more unit tests --- final_task/pycalc/calculator.py | 27 +++-- final_task/pycalc/pycalc.py | 4 +- final_task/pycalc/tester.py | 195 +++++++++++++++++++++++++++----- 3 files changed, 181 insertions(+), 45 deletions(-) diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index b2a94f2..4a6d364 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -23,23 +23,27 @@ class Calculator: ('==', lambda a, b: a == b) ) - FUNCTIONS = ( + BUILTINS = ( ('pow', lambda a, b, c: pow(a, b)), ('abs', lambda a: abs(a)), ('round', lambda a: round(a)), ('ctan', lambda a: 1 / math.tan(a)) ) - def __init__(self, validator): + def __init__(self, validator, modules=None): + """Initializes validator and user modules.""" + self._validator = validator self._modules = [math] + self.import_modules(modules) + self._constants = self.get_reserved(c=False) + self._functions = self.get_reserved(c=True) - def calc_start(self, expression, modules=None): + def calc_start(self, expression): """Entry point of calculating. Validates, transforms and finally calculates given expression. Returns calculating result.""" self._validator.validate(expression) - self.import_modules(modules) expression = self.transform(expression) expression = self.replace_constants(expression) @@ -98,7 +102,7 @@ def calculate(self, expression=None): for sign, func in self.BINARIES: while True: - sign_pos = self.find_sign(expression, sign) + sign_pos = self.find_sign_pos(expression, sign) if sign_pos == -1: break @@ -119,7 +123,7 @@ def calculate(self, expression=None): return expression.strip('[]') - def find_sign(self, expression, sign): + def find_sign_pos(self, expression, sign): """Returns a position of given sign in the expression (-1 if not found).""" @@ -147,9 +151,8 @@ def calculate_functions(self, expression): if func_name in ('False', 'True'): continue - functions = self.get_reserved(c=True) func = None - if func_name in functions: + if func_name in self._functions: func = self.get_reserved_by_name(func_name) else: self._validator.assert_error( @@ -204,7 +207,7 @@ def get_reserved(self, c=False): result.append(d) if c: - for func_name, _ in self.FUNCTIONS: + for func_name, _ in self.BUILTINS: result.append(func_name) # Sort here is used to prevent replacing @@ -215,8 +218,8 @@ def get_reserved(self, c=False): def replace_constants(self, expression): """Finds constants in imported and user modules and builtins.""" - const_pattern = '|'.join(self.get_reserved(c=False)) - func_pattern = '|'.join(self.get_reserved(c=True)) + const_pattern = '|'.join(self._constants) + func_pattern = '|'.join(self._functions) pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' cases = re.finditer(pattern, expression) @@ -249,7 +252,7 @@ def get_reserved_by_name(self, requested_name): obj = getattr(m, requested_name) return obj - for func_name, func in self.FUNCTIONS: + for func_name, func in self.BUILTINS: if func_name == requested_name: return func diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index f30253b..291c656 100755 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -8,12 +8,12 @@ def main(): argparser = Argparser() validator = Validator() - calc = Calculator(validator) args = argparser.parse_input() expression = args.expression[0] modules = args.modules - result = calc.calc_start(expression, modules) + calc = Calculator(validator, modules) + result = calc.calc_start(expression) print(result) diff --git a/final_task/pycalc/tester.py b/final_task/pycalc/tester.py index 2d2957d..9a96379 100755 --- a/final_task/pycalc/tester.py +++ b/final_task/pycalc/tester.py @@ -1,43 +1,176 @@ """Unittests for calculator.""" +import math import unittest from .validator import Validator from .calculator import Calculator +class ValidatorTester(unittest.TestCase): + """Handles all validator testing operations.""" + + val = Validator() + + def test_validate(self): + """Tests most common cases in validator.""" + + with self.assertRaises(SystemExit): + self.val.validate('((') + + with self.assertRaises(SystemExit): + self.val.validate('') + + with self.assertRaises(SystemExit): + self.val.validate('1 2 3 4 5 + 6') + + with self.assertRaises(SystemExit): + self.val.validate('123 > = 5') + + def test_check(self): + """Tests fast checking function.""" + + with self.assertRaises(SystemExit): + self.val.check('*', None, 1) + + with self.assertRaises(SystemExit): + self.val.check('^', -1, 1.5) + + class CalculatorTester(unittest.TestCase): - """Handles all testing operation.""" + """Handles all calculator testing operations.""" + + val = Validator() + calc = Calculator(val) + + def test_calc_start(self): + """Checks most common calculating cases.""" - def test_calculate(self): val = Validator() calc = Calculator(val) - self.assertEqual(calc.calc_start('2'), 2) - self.assertEqual(calc.calc_start('-2'), -2) - - self.assertEqual(calc.calc_start('2+2'), 4) - self.assertEqual(calc.calc_start('2-2'), 0) - self.assertEqual(calc.calc_start('2*2'), 4) - self.assertEqual(calc.calc_start('2/2'), 1.0) - self.assertEqual(calc.calc_start('15%2'), 1) - self.assertEqual(calc.calc_start('2**16'), 65536) - - self.assertEqual(calc.calc_start('2*log10(100)*3'), 12.0) - self.assertEqual(calc.calc_start('pow(2,2)sqrt(625)'), 100.0) - self.assertEqual(calc.calc_start('pow(2, 256)'), 2 ** 256) - - self.assertEqual(calc.calc_start('2+2*2'), 6) - self.assertEqual(calc.calc_start('2/2*2'), 2) - self.assertEqual(calc.calc_start('2*2/2'), 2) - - self.assertEqual(calc.calc_start('5*(5+5)'), 50) - self.assertEqual(calc.calc_start('5-(5+5)'), -5) - self.assertEqual(calc.calc_start('5(5+5)5'), 250) - self.assertEqual(calc.calc_start('2(2+2)(2+2)2'), 64) - self.assertEqual(calc.calc_start('2(2((2+2)2))2'), 64) - - self.assertEqual(calc.calc_start( - '(10-20)(20+10)log10(100)(sqrt(25)+5)'), -6000) - self.assertEqual(calc.calc_start('32-32*2+50-5**2*2'), -32) - self.assertEqual(calc.calc_start( - '9*(2+150*(650/5)-190+445/(20-25))+12+90/5-173036/2*2'), 1.0) + tests = ( + ('2', 2), + ('-2', -2), + ('2', 2), + ('-2', -2), + ('2+2', 2+2), + ('2-2', 2-2), + ('2*2', 2*2), + ('2/2', 2/2), + ('15%2', 15 % 2), + ('2**16', 2**16), + ('2*log10(100)*3', 12.0), + ('pow(2,2)sqrt(625)', 100.0), + ('pow(2, 256)', 2 ** 256), + ('eexp(e)', 41.193555674716116), + ('2+2*2', 2+2*2), + ('2/2*2', 2/2*2), + ('2*2/2', 2*2/2), + ('5*(5+5)', 5*(5+5)), + ('5-(5+5)', 5-(5+5)), + ('5(5+5)5', 5*(5+5)*5), + ('2(2+2)(2+2)2', 2*(2+2)*(2+2)*2), + ('2(2((2+2)2))2', 2*(2*((2+2)*2))*2), + ('(10-20)(20+10)log10(100)(sqrt(25)+5)', -6000), + ('32-32*2+50-5**2*2', 32-32*2+50-5**2*2), + ('9*(2+150*(650/5)-190+445/(20-25))+12+90/5-173036/2*2', 1.0) + ) + + for test, result in tests: + self.assertEqual(self.calc.calc_start(test), result) + + def test_find_left_num(self): + """Tests if left number is found correctly.""" + + result = self.calc.find_left_num('-125.125+10', 8) + self.assertEqual(result, '-125.125') + + def test_find_right_num(self): + """Tests if right number is found correctly.""" + + result = self.calc.find_right_num('-125.125+10', 8) + self.assertEqual(result, '10') + + def test_find_sign_pos(self): + """Tests if sign position is found correctly.""" + + result = self.calc.find_sign_pos('12345678^87654321', '^') + self.assertEqual(result, 8) + + def test_calculate_functions(self): + """Tests function calculating method.""" + + result = self.calc.calculate_functions('log10(100)*2') + self.assertEqual(result, '(2.0)*2') + + def test_get_func_args(self): + """Tests arguments fonding method.""" + + result = self.calc.get_func_args( + '2*2*2*any_func(5,(6+7),((8+9)+10))', 'any_func', 6 + ) + self.assertEqual(result, (['5', '(6+7)', '((8+9)+10)'], 34)) + + def test_replace_constants(self): + """Tests if constants are replaced correctly.""" + + result = self.calc.replace_constants('2pietau') + self.assertEqual( + result, + '2(3.141592653589793)(2.718281828459045)(6.283185307179586)' + ) + + def test_get_reserved_by_name(self): + """Tests if function object is got properly by its name.""" + + result = self.calc.get_reserved_by_name('log') + self.assertEqual(result, math.log) + + def test_convert(self): + """Tests correctness of string to number/boolean conversion.""" + + self.assertEqual(self.calc.convert('2'), 2) + self.assertEqual(self.calc.convert('.1'), 0.1) + self.assertEqual(self.calc.convert('True'), True) + + def test_convert_arguments(self): + """Tests function arguments conversion.""" + + result = self.calc.convert_arguments(['2', 'log10(100)', '(2+2)']) + self.assertEqual(result, [2, 2.0, 4]) + + def test_handle_subtraction(self): + """Tests correctness of subtraction replacing.""" + + result = self.calc.handle_subtraction('2-1000*2(-100-50)') + self.assertEqual(result, '2+-1000*2(-100+-50)') + + def test_handle_implicit_multiplication(self): + """Tests correctness of implicit multiplication replacing.""" + + result = self.calc.handle_implicit_multiplication('2(10)(10)5') + self.assertEqual(result, '2*(10)*(10)*5') + + def test_handle_extra_signs(self): + """Tests correctness of getting rid of extra signs.""" + + result = self.calc.handle_extra_signs('1----1+++++++1-+-+-+-+---+2') + self.assertEqual(result, '1+1+1+-2') + + def test_calculate_elementary(self): + """Tests elementary binary operations calculating.""" + + result = self.calc.calculate_elementary('*', '1', '2') + self.assertEqual(result, 2) + + def test_calculate_nested(self): + """Checks correctness of calculating nested expressions.""" + + result = self.calc.calculate_nested('((2*(((10+10)))))') + self.assertEqual(result, '[40]') + + def test_get_nested(self): + """Checks correctness of getting nested with no nested inside it.""" + + result = self.calc.get_nested('(10*10(10*10)(10*10(20*20)))') + self.assertEqual(result, '(10*10)') From 272d99cbef1c30e918d4f5ca9003cca80b0eb69c Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Fri, 16 Nov 2018 22:46:59 +0300 Subject: [PATCH 13/15] Made code review with few corrections --- final_task/pycalc/calculator.py | 77 ++++++++++++++------------------- final_task/pycalc/tester.py | 5 +-- final_task/pycalc/validator.py | 5 ++- 3 files changed, 37 insertions(+), 50 deletions(-) diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index 4a6d364..3ddaef0 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -24,20 +24,21 @@ class Calculator: ) BUILTINS = ( - ('pow', lambda a, b, c: pow(a, b)), ('abs', lambda a: abs(a)), - ('round', lambda a: round(a)), - ('ctan', lambda a: 1 / math.tan(a)) + ('round', lambda a: round(a)) ) + IMPLICIT_MUL_PATTERNS = ( + r'[0-9][A-Za-z][^\-+]', r'[\)\]][0-9]', r'[0-9][\[\(]', r'\)\(|\]\[') + def __init__(self, validator, modules=None): """Initializes validator and user modules.""" self._validator = validator self._modules = [math] self.import_modules(modules) - self._constants = self.get_reserved(c=False) - self._functions = self.get_reserved(c=True) + self._constants = self.get_reserved(_callable=False) + self._functions = self.get_reserved(_callable=True) def calc_start(self, expression): """Entry point of calculating. Validates, transforms and finally @@ -46,6 +47,7 @@ def calc_start(self, expression): self._validator.validate(expression) expression = self.transform(expression) + expression = self.handle_subtraction(expression) expression = self.replace_constants(expression) expression = self.calculate_functions(expression) @@ -68,7 +70,6 @@ def transform(self, expression): expression = expression.strip('"') expression = expression.replace(' ', '') expression = expression.replace('**', '^') - expression = self.handle_subtraction(expression) return expression @@ -76,6 +77,7 @@ def find_left_num(self, expression, sign_pos): """Returns a number that lays left from the sign_pos (or None if it doesn't exist.""" + # Any number (including exponential ones), near the end of the string pattern = r'([0-9\[\]\.\-]|(e\+)|(e\-))+$' num = re.search(pattern, expression[:sign_pos]) if num is None: @@ -86,13 +88,14 @@ def find_right_num(self, expression, sign_pos): """Returns a number that lays right from the sign_pos (or None if it doesn't exist.""" + # Any number (including exponential ones) near the start of the string pattern = r'^([0-9\[\]\.\-]|(e\+)|(e\-))+' num = re.search(pattern, expression[sign_pos + 1:]) if num is None: self._validator.assert_error("please, check your expression.") return num.group(0) - def calculate(self, expression=None): + def calculate(self, expression): """Recursive function that divides the expression, calculates its right and left parts and whole result.""" @@ -106,12 +109,9 @@ def calculate(self, expression=None): if sign_pos == -1: break - if sign == '^': - left = self.find_left_num(expression, sign_pos) - if left.find(']') == -1: - left = left.replace('-', '') - else: - left = self.find_left_num(expression, sign_pos) + left = self.find_left_num(expression, sign_pos) + if sign == '^' and left.find(']') == -1: + left = left.replace('-', '') slen = len(sign) - 1 right = self.find_right_num(expression, sign_pos + slen) @@ -127,7 +127,6 @@ def find_sign_pos(self, expression, sign): """Returns a position of given sign in the expression (-1 if not found).""" - sign_pos = None if sign == '^': sign_pos = expression.rfind(sign) elif sign == '+': @@ -144,20 +143,14 @@ def calculate_functions(self, expression): contains only elementary binary operations.""" # List reversion here makes it possible to calculate nested functions - pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' + pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' # Any word, may end with a digit func_name_list = re.findall(pattern, expression)[::-1] for func_name in func_name_list: if func_name in ('False', 'True'): continue - func = None - if func_name in self._functions: - func = self.get_reserved_by_name(func_name) - else: - self._validator.assert_error( - "no such function " + func_name + ".") - + func = self.get_reserved_by_name(func_name) fpos = expression.rfind(func_name) args, arg_end = self.get_func_args(expression, func_name, fpos) @@ -173,7 +166,7 @@ def calculate_functions(self, expression): "please, check function " + func_name + ".") expression = expression.replace( - expression[fpos:arg_end], '(' + str(result) + ')', 1 + expression[fpos:arg_end], '[' + str(result) + ']', 1 ) return expression @@ -196,22 +189,21 @@ def get_func_args(self, expression, func_name, func_pos): return argument_list, arg_end - def get_reserved(self, c=False): + def get_reserved(self, _callable=False): """Returns a list of all the constants found in imported modules.""" result = [] for m in self._modules: for d in dir(m): obj = getattr(m, d) - if callable(obj) is c and not d.startswith('_'): + if callable(obj) is _callable and not d.startswith('_'): result.append(d) - if c: + if _callable: for func_name, _ in self.BUILTINS: result.append(func_name) - # Sort here is used to prevent replacing - # letters of long reserved names + # Sort is used to prevent replacing parts of long reserved names result.sort(key=lambda a: len(a), reverse=True) return result @@ -227,8 +219,7 @@ def replace_constants(self, expression): c_str = case.group() c_pos = case.start() - # Upper is used to prevent replacing - # letters in functions (e. g. "eexp(e)") + # Upper is used to prevent replacing parts of functions replaced = c_str funcs = re.findall(func_pattern, replaced) for f in funcs: @@ -252,9 +243,12 @@ def get_reserved_by_name(self, requested_name): obj = getattr(m, requested_name) return obj - for func_name, func in self.BUILTINS: - if func_name == requested_name: - return func + for name, obj in self.BUILTINS: + if name == requested_name: + return obj + + self._validator.assert_error( + "no such reserved name " + requested_name + ".") @staticmethod def convert(a): @@ -288,7 +282,7 @@ def handle_subtraction(self, expression): """Modifies subtractions in given expression to make them calculator friendly.""" - pattern = r'[0-9\]]\-' + pattern = r'[0-9\]]\-' # Any digit followed my minus cases = re.findall(pattern, expression) for c in cases: expression = expression.replace(c, c[0] + '+' + c[1]) @@ -298,11 +292,7 @@ def handle_subtraction(self, expression): def handle_implicit_multiplication(self, expression): """Replaces all implicit multiplication cases with obvious ones.""" - patterns = ( - r'[0-9][A-Za-z][^\-+]', r'[\)\]][0-9]', - r'[0-9][\[\(]', r'\)\(|\]\[' - ) - for p in patterns: + for p in self.IMPLICIT_MUL_PATTERNS: cases = re.findall(p, expression) for c in cases: expression = expression.replace(c, c[0] + '*' + c[1]) @@ -312,11 +302,10 @@ def handle_implicit_multiplication(self, expression): def handle_extra_signs(self, expression): """Gets rid of extra pluses and minuses in given expression.""" - pattern = r'[-+]{2,}' + pattern = r'[-+]{2,}' # Two or more pluses and minuses cases = re.findall(pattern, expression) for c in cases: - minus_count = c.count('-') - if minus_count % 2 == 0: + if c.count('-') % 2 == 0: expression = expression.replace(c, '+', 1) else: expression = expression.replace(c, '-', 1) @@ -330,7 +319,7 @@ def calculate_elementary(self, operation, *args): result = None - args = list(re.sub(r'[\[\]]', '', a) for a in args) + args = list(re.sub(r'[\[\]]', '', a) for a in args) # Get rid of [ ] args = list(self.handle_extra_signs(a) for a in args) converted_args = [] @@ -363,7 +352,7 @@ def calculate_nested(self, expression): @staticmethod def get_nested(expression): """Finds and returns nested expression (with no nested inside it) if - it exists, else returns None.""" + it exists, else returns None.""" # From '(' to ')' blocking any parentheses inside nested = re.search(r'\([^()]*\)', expression) diff --git a/final_task/pycalc/tester.py b/final_task/pycalc/tester.py index 9a96379..154f9e5 100755 --- a/final_task/pycalc/tester.py +++ b/final_task/pycalc/tester.py @@ -45,9 +45,6 @@ class CalculatorTester(unittest.TestCase): def test_calc_start(self): """Checks most common calculating cases.""" - val = Validator() - calc = Calculator(val) - tests = ( ('2', 2), ('-2', -2), @@ -101,7 +98,7 @@ def test_calculate_functions(self): """Tests function calculating method.""" result = self.calc.calculate_functions('log10(100)*2') - self.assertEqual(result, '(2.0)*2') + self.assertEqual(result, '[2.0]*2') def test_get_func_args(self): """Tests arguments fonding method.""" diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py index d7d80f7..a180202 100755 --- a/final_task/pycalc/validator.py +++ b/final_task/pycalc/validator.py @@ -15,7 +15,7 @@ def validate(self, expression): if expression.count('(') != expression.count(')'): self.assert_error("brackets are not balanced.") - pattern = r'[0-9]+\s+[0-9]+' + pattern = r'[0-9\.]+\s+[0-9\.]+' if re.search(pattern, expression) is not None: self.assert_error("ambiguous spaces between numbers.") @@ -34,6 +34,7 @@ def check(self, sign, left, right): "negative number cannot be raised to fractional power." ) - def assert_error(self, error_text, exitcode=1): + @staticmethod + def assert_error(error_text, exitcode=1): print("ERROR: " + error_text) exit(exitcode) From 6ab86fe363e62814d34c32ded2c94d6e8bf59a7e Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Sat, 8 Dec 2018 23:37:31 +0300 Subject: [PATCH 14/15] Removed unnecessary docstrings. Added correct Zero division Error handling --- final_task/pycalc/calculator.py | 74 +++++++++++---------------------- final_task/pycalc/pycalc.py | 6 +-- final_task/pycalc/tester.py | 47 +++------------------ final_task/pycalc/validator.py | 10 +++-- 4 files changed, 41 insertions(+), 96 deletions(-) diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py index 3ddaef0..4b466ab 100755 --- a/final_task/pycalc/calculator.py +++ b/final_task/pycalc/calculator.py @@ -1,12 +1,12 @@ -"""Main module of the calculator.""" +"""Calculator core module.""" import math import re class Calculator: - """Handles all operations related to the conversion and evaluation - of expressions.""" + """Handles all operations related to the + conversion and evaluation of expressions.""" BINARIES = ( ('^', lambda a, b: a ** b), # ** @@ -32,8 +32,6 @@ class Calculator: r'[0-9][A-Za-z][^\-+]', r'[\)\]][0-9]', r'[0-9][\[\(]', r'\)\(|\]\[') def __init__(self, validator, modules=None): - """Initializes validator and user modules.""" - self._validator = validator self._modules = [math] self.import_modules(modules) @@ -41,8 +39,8 @@ def __init__(self, validator, modules=None): self._functions = self.get_reserved(_callable=True) def calc_start(self, expression): - """Entry point of calculating. Validates, transforms and finally - calculates given expression. Returns calculating result.""" + """Entry point of calculating. Prepares the expression for + calculating, calculates it and returns the result.""" self._validator.validate(expression) @@ -55,15 +53,14 @@ def calc_start(self, expression): return self.convert(result) def import_modules(self, modules): - """Imports all modules from given list.""" - if modules is not None: for m in modules: new_module = __import__(m) - self._modules.append(new_module) + self._modules.insert(0, new_module) - def transform(self, expression): - """Transforms the expression into Calculator friendly form.""" + @staticmethod + def transform(expression): + """Removes and replaces extra and ambiguous symbols.""" expression = expression.lower() expression = expression.strip("'") @@ -74,8 +71,7 @@ def transform(self, expression): return expression def find_left_num(self, expression, sign_pos): - """Returns a number that lays left from the sign_pos (or - None if it doesn't exist.""" + """Returns a number that lays left from the sign_pos.""" # Any number (including exponential ones), near the end of the string pattern = r'([0-9\[\]\.\-]|(e\+)|(e\-))+$' @@ -85,8 +81,7 @@ def find_left_num(self, expression, sign_pos): return num.group(0) def find_right_num(self, expression, sign_pos): - """Returns a number that lays right from the sign_pos (or - None if it doesn't exist.""" + """Returns a number that lays right from the sign_pos.""" # Any number (including exponential ones) near the start of the string pattern = r'^([0-9\[\]\.\-]|(e\+)|(e\-))+' @@ -96,8 +91,8 @@ def find_right_num(self, expression, sign_pos): return num.group(0) def calculate(self, expression): - """Recursive function that divides the expression, calculates its - right and left parts and whole result.""" + """Calculates the expression step-by-step + according to the sign priority.""" expression = self.calculate_nested(expression) expression = self.handle_implicit_multiplication(expression) @@ -124,8 +119,7 @@ def calculate(self, expression): return expression.strip('[]') def find_sign_pos(self, expression, sign): - """Returns a position of given sign in the - expression (-1 if not found).""" + """Returns the position of given sign (-1 if not found).""" if sign == '^': sign_pos = expression.rfind(sign) @@ -139,8 +133,7 @@ def find_sign_pos(self, expression, sign): return sign_pos def calculate_functions(self, expression): - """Calculates all founded functions and returns an expression that - contains only elementary binary operations.""" + """Substitutes functions with the result of their calculation.""" # List reversion here makes it possible to calculate nested functions pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' # Any word, may end with a digit @@ -172,9 +165,6 @@ def calculate_functions(self, expression): return expression def get_func_args(self, expression, func_name, func_pos): - """Finds all the arguments of the function, - located on the func_pos (including nested ones).""" - arg_start = func_pos + len(func_name) arg_end = arg_start + expression[arg_start:].find(')') arguments = expression[arg_start:arg_end] @@ -190,7 +180,8 @@ def get_func_args(self, expression, func_name, func_pos): return argument_list, arg_end def get_reserved(self, _callable=False): - """Returns a list of all the constants found in imported modules.""" + """Returns a list of all functions and + constants found in imported modules.""" result = [] for m in self._modules: @@ -203,16 +194,12 @@ def get_reserved(self, _callable=False): for func_name, _ in self.BUILTINS: result.append(func_name) - # Sort is used to prevent replacing parts of long reserved names - result.sort(key=lambda a: len(a), reverse=True) return result def replace_constants(self, expression): - """Finds constants in imported and user modules and builtins.""" - const_pattern = '|'.join(self._constants) - func_pattern = '|'.join(self._functions) - pattern = r'[A-Za-z_]+[A-Za-z0-9_]*' + func_pattern = r'\(|'.join(self._functions) + r'\(' + pattern = r'[A-Za-z_]+[A-Za-z0-9_]*[\(]*' cases = re.finditer(pattern, expression) for case in cases: @@ -223,7 +210,7 @@ def replace_constants(self, expression): replaced = c_str funcs = re.findall(func_pattern, replaced) for f in funcs: - replaced = re.sub(f, f.upper(), replaced) + replaced = replaced.replace(f, f.upper()) constants = re.findall(const_pattern, c_str) for const in constants: @@ -236,7 +223,7 @@ def replace_constants(self, expression): return expression.lower() def get_reserved_by_name(self, requested_name): - """Finds a function or constant by name.""" + """Returns function (or constant) object, exits if not found.""" for m in self._modules: if hasattr(m, requested_name): @@ -252,8 +239,6 @@ def get_reserved_by_name(self, requested_name): @staticmethod def convert(a): - """Converts an argument to int, bool or float.""" - if not isinstance(a, str): return a @@ -269,8 +254,6 @@ def convert(a): return a def convert_arguments(self, args): - """Returns a list of converted arguments.""" - converted_args = [] for a in args: result_a = self.calc_start(a) @@ -279,8 +262,8 @@ def convert_arguments(self, args): return converted_args def handle_subtraction(self, expression): - """Modifies subtractions in given expression to make them - calculator friendly.""" + """Modifies subtractions in given expression + to make them calculator friendly.""" pattern = r'[0-9\]]\-' # Any digit followed my minus cases = re.findall(pattern, expression) @@ -290,8 +273,6 @@ def handle_subtraction(self, expression): return expression def handle_implicit_multiplication(self, expression): - """Replaces all implicit multiplication cases with obvious ones.""" - for p in self.IMPLICIT_MUL_PATTERNS: cases = re.findall(p, expression) for c in cases: @@ -314,9 +295,6 @@ def handle_extra_signs(self, expression): return expression def calculate_elementary(self, operation, *args): - """Calculates elementary binary operations like addition, subtraction, - multiplication, division etc.""" - result = None args = list(re.sub(r'[\[\]]', '', a) for a in args) # Get rid of [ ] @@ -337,8 +315,6 @@ def calculate_elementary(self, operation, *args): return result def calculate_nested(self, expression): - """Returns the result of nested expression calculating.""" - while True: nested = self.get_nested(expression) if nested is None: @@ -351,8 +327,8 @@ def calculate_nested(self, expression): @staticmethod def get_nested(expression): - """Finds and returns nested expression (with no nested inside it) if - it exists, else returns None.""" + """Finds and returns nested expression (with no + nested inside it) if it exists, else returns None.""" # From '(' to ')' blocking any parentheses inside nested = re.search(r'\([^()]*\)', expression) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index 291c656..35fd239 100755 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -1,8 +1,8 @@ """Entry point of pycalc project.""" -from .argparser import Argparser -from .calculator import Calculator -from .validator import Validator +from argparser import Argparser +from calculator import Calculator +from validator import Validator def main(): diff --git a/final_task/pycalc/tester.py b/final_task/pycalc/tester.py index 154f9e5..d1d1f94 100755 --- a/final_task/pycalc/tester.py +++ b/final_task/pycalc/tester.py @@ -2,18 +2,16 @@ import math import unittest -from .validator import Validator -from .calculator import Calculator +from validator import Validator +from calculator import Calculator class ValidatorTester(unittest.TestCase): - """Handles all validator testing operations.""" + """Tests error cases.""" val = Validator() def test_validate(self): - """Tests most common cases in validator.""" - with self.assertRaises(SystemExit): self.val.validate('((') @@ -27,24 +25,21 @@ def test_validate(self): self.val.validate('123 > = 5') def test_check(self): - """Tests fast checking function.""" - with self.assertRaises(SystemExit): self.val.check('*', None, 1) + with self.assertRaises(SystemExit): + self.val.check('/', 10, 0) + with self.assertRaises(SystemExit): self.val.check('^', -1, 1.5) class CalculatorTester(unittest.TestCase): - """Handles all calculator testing operations.""" - val = Validator() calc = Calculator(val) def test_calc_start(self): - """Checks most common calculating cases.""" - tests = ( ('2', 2), ('-2', -2), @@ -77,40 +72,28 @@ def test_calc_start(self): self.assertEqual(self.calc.calc_start(test), result) def test_find_left_num(self): - """Tests if left number is found correctly.""" - result = self.calc.find_left_num('-125.125+10', 8) self.assertEqual(result, '-125.125') def test_find_right_num(self): - """Tests if right number is found correctly.""" - result = self.calc.find_right_num('-125.125+10', 8) self.assertEqual(result, '10') def test_find_sign_pos(self): - """Tests if sign position is found correctly.""" - result = self.calc.find_sign_pos('12345678^87654321', '^') self.assertEqual(result, 8) def test_calculate_functions(self): - """Tests function calculating method.""" - result = self.calc.calculate_functions('log10(100)*2') self.assertEqual(result, '[2.0]*2') def test_get_func_args(self): - """Tests arguments fonding method.""" - result = self.calc.get_func_args( '2*2*2*any_func(5,(6+7),((8+9)+10))', 'any_func', 6 ) self.assertEqual(result, (['5', '(6+7)', '((8+9)+10)'], 34)) def test_replace_constants(self): - """Tests if constants are replaced correctly.""" - result = self.calc.replace_constants('2pietau') self.assertEqual( result, @@ -118,56 +101,38 @@ def test_replace_constants(self): ) def test_get_reserved_by_name(self): - """Tests if function object is got properly by its name.""" - result = self.calc.get_reserved_by_name('log') self.assertEqual(result, math.log) def test_convert(self): - """Tests correctness of string to number/boolean conversion.""" - self.assertEqual(self.calc.convert('2'), 2) self.assertEqual(self.calc.convert('.1'), 0.1) self.assertEqual(self.calc.convert('True'), True) def test_convert_arguments(self): - """Tests function arguments conversion.""" - result = self.calc.convert_arguments(['2', 'log10(100)', '(2+2)']) self.assertEqual(result, [2, 2.0, 4]) def test_handle_subtraction(self): - """Tests correctness of subtraction replacing.""" - result = self.calc.handle_subtraction('2-1000*2(-100-50)') self.assertEqual(result, '2+-1000*2(-100+-50)') def test_handle_implicit_multiplication(self): - """Tests correctness of implicit multiplication replacing.""" - result = self.calc.handle_implicit_multiplication('2(10)(10)5') self.assertEqual(result, '2*(10)*(10)*5') def test_handle_extra_signs(self): - """Tests correctness of getting rid of extra signs.""" - result = self.calc.handle_extra_signs('1----1+++++++1-+-+-+-+---+2') self.assertEqual(result, '1+1+1+-2') def test_calculate_elementary(self): - """Tests elementary binary operations calculating.""" - result = self.calc.calculate_elementary('*', '1', '2') self.assertEqual(result, 2) def test_calculate_nested(self): - """Checks correctness of calculating nested expressions.""" - result = self.calc.calculate_nested('((2*(((10+10)))))') self.assertEqual(result, '[40]') def test_get_nested(self): - """Checks correctness of getting nested with no nested inside it.""" - result = self.calc.get_nested('(10*10(10*10)(10*10(20*20)))') self.assertEqual(result, '(10*10)') diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py index a180202..26ce71f 100755 --- a/final_task/pycalc/validator.py +++ b/final_task/pycalc/validator.py @@ -1,11 +1,12 @@ -"""Tools for validating given expressions.""" +""" + Validator checks for possible error + before and during the calculation. +""" import re class Validator: - """Validator checks given expression for possible errors.""" - def validate(self, expression): """Fully validates given expression for user error""" @@ -29,6 +30,9 @@ def check(self, sign, left, right): if left is None or right is None: self.assert_error("please, check your expression.") + if sign in ('/', '//', '%') and right == 0: + self.assert_error("got a zero division error.") + if sign == '^' and left < 0 and isinstance(right, float): self.assert_error( "negative number cannot be raised to fractional power." From 31024e6a3661425fc85cd6a539b86da23bf98d30 Mon Sep 17 00:00:00 2001 From: Alexander Gutyra Date: Sat, 8 Dec 2018 23:38:35 +0300 Subject: [PATCH 15/15] Fixed module imports --- final_task/pycalc/pycalc.py | 6 +++--- final_task/pycalc/tester.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py index 35fd239..291c656 100755 --- a/final_task/pycalc/pycalc.py +++ b/final_task/pycalc/pycalc.py @@ -1,8 +1,8 @@ """Entry point of pycalc project.""" -from argparser import Argparser -from calculator import Calculator -from validator import Validator +from .argparser import Argparser +from .calculator import Calculator +from .validator import Validator def main(): diff --git a/final_task/pycalc/tester.py b/final_task/pycalc/tester.py index d1d1f94..bffafde 100755 --- a/final_task/pycalc/tester.py +++ b/final_task/pycalc/tester.py @@ -2,8 +2,8 @@ import math import unittest -from validator import Validator -from calculator import Calculator +from .validator import Validator +from .calculator import Calculator class ValidatorTester(unittest.TestCase):