diff --git a/final_task/calculator/__init__.py b/final_task/calculator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/calculator/checker/__init__.py b/final_task/calculator/checker/__init__.py new file mode 100644 index 00000000..867c39ab --- /dev/null +++ b/final_task/calculator/checker/__init__.py @@ -0,0 +1,7 @@ +from .checker import ( + check_spaces, + check_brackets, + check_constant, + check_function, + check_expression, +) diff --git a/final_task/calculator/checker/checker.py b/final_task/calculator/checker/checker.py new file mode 100644 index 00000000..2ece1ba3 --- /dev/null +++ b/final_task/calculator/checker/checker.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to test the mathematical expression for correctness. + +Example: + check_spaces(' 1+ 3 / 2') + >>> '1+3/2' + + check_brackets('2*(3+5)') + >>> '2*(3+5)' + + lib = { + 'e': 2.718281828459045, + 'sum': sum + } + check_function('sum(100, 50)', lib) + >>> 'sum(100, 50)' +""" + +import re +from ..library import Library +from ..operators import ( + LEFT_BRACKET, + RIGHT_BRACKET, +) +from ..regexp import ( + REGEXP_INCORECT_EXPRETION, + REGEXP_CONSTANT, + REGEXP_DIGIT, + REGEXP_FUNCTION, +) + + +def check_spaces(expr: str) -> str: + """ + Checks if an expression has the wrong elements. + + Args: + expr (str): String mathematical expression. + + Returns: + str: cleared expression from spaces. + + Raises: + ValueError: If `expr` is not correct`. + """ + matches = re.findall(REGEXP_INCORECT_EXPRETION, expr) + if matches: + raise ValueError('expression is not correct') + + return expr.replace(' ', '') + + +def check_brackets(expr: str): + """ + Checks if all brackets have a pair. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + stack = [] + for symbol in expr: + if symbol == LEFT_BRACKET: + stack.append(symbol) + elif symbol == RIGHT_BRACKET and (not stack or stack.pop() != LEFT_BRACKET): + raise ValueError('brackets are not balanced') + + if stack: + raise ValueError('brackets are not balanced') + + +def check_constant(expr: str, library: Library): + """ + Checks if all constants in the expression are available. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Raises: + ValueError: If `expr` is not correct`. + """ + matches = re.finditer(REGEXP_CONSTANT, expr) + for match in matches: + name = match.group('name') + + if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): + continue + + if name[0].isdigit(): + raise ValueError(f'constant {name} can not start with digit') + + if name not in library or callable(library[name]): + raise ValueError(f'there is no such constant {name}') + + +def check_function(expr: str, library: Library): + """ + Checks if all functions in the expression are available. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Raises: + ValueError: If `expr` is not correct`. + """ + matches = re.finditer(REGEXP_FUNCTION, expr) + for match in matches: + name = match.group('name') + pattern = match.group('pattern') + + if name[0].isdigit(): + raise ValueError(f'function {pattern} can not start with digit') + + if name not in library or not callable(library[name]): + raise ValueError(f'there is no such function {pattern}') + + +def check_expression(expr: str, library: Library) -> str: + """ + Checks the expression for correctness. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Returns: + str: cleared expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + expr = check_spaces(expr) + check_brackets(expr) + check_constant(expr, library) + check_function(expr, library) + + return expr diff --git a/final_task/calculator/checker/test_checker.py b/final_task/calculator/checker/test_checker.py new file mode 100644 index 00000000..ada278e8 --- /dev/null +++ b/final_task/calculator/checker/test_checker.py @@ -0,0 +1,115 @@ +import unittest +from ..library import Library +from .checker import ( + check_brackets, + check_constant, + check_expression, + check_function, + check_spaces, +) + + +class TestCheckFunction(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib = Library('math') + + def test_check_spaces(self): + with self.subTest("Throws error if spaces is not correct"): + self.assertRaises(ValueError, lambda: check_spaces('')) + self.assertRaises(ValueError, lambda: check_spaces('------')) + self.assertRaises(ValueError, lambda: check_spaces('-')) + self.assertRaises(ValueError, lambda: check_spaces('+')) + self.assertRaises(ValueError, lambda: check_spaces('1-')) + self.assertRaises(ValueError, lambda: check_spaces('1 + 1 2 3 4 5 6 ')) + self.assertRaises(ValueError, lambda: check_spaces('* *')) + self.assertRaises(ValueError, lambda: check_spaces('/ /')) + self.assertRaises(ValueError, lambda: check_spaces('/ *')) + self.assertRaises(ValueError, lambda: check_spaces('+ *')) + self.assertRaises(ValueError, lambda: check_spaces('1 2')) + self.assertRaises(ValueError, lambda: check_spaces('= =')) + self.assertRaises(ValueError, lambda: check_spaces('! =')) + self.assertRaises(ValueError, lambda: check_spaces('<-+!')) + self.assertRaises(ValueError, lambda: check_spaces('==7')) + self.assertRaises(ValueError, lambda: check_spaces('1 + 2(3 * 4))')) + self.assertRaises(ValueError, lambda: check_spaces('1 = = 2')) + self.assertRaises(ValueError, lambda: check_spaces('1<>2')) + self.assertRaises(ValueError, lambda: check_spaces('1><2')) + + with self.subTest("removes spaces and returns new expretion"): + self.assertEqual(check_spaces('1 + 2'), '1+2') + self.assertEqual(check_spaces('1-2'), '1-2') + self.assertEqual(check_spaces('1 * - 2'), '1*-2') + self.assertEqual(check_spaces('1 == 2'), '1==2') + self.assertEqual(check_spaces('1 <= 2'), '1<=2') + self.assertEqual(check_spaces('1 - sin (1, 2, 3) + - 2'), '1-sin(1,2,3)+-2') + self.assertEqual(check_spaces('sin(pi/2)'), 'sin(pi/2)') + self.assertTrue(check_spaces('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))')) + self.assertTrue(check_spaces('time()-e-(1+1)/60+1-1*1//10000%1000^2==1==1<=3>=5<1>1')) + + val = ('sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))' + '+cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)') + self.assertTrue(check_spaces(val)) + + def test_check_brackets(self): + with self.subTest("Throws error if brackets are not unpaired"): + self.assertRaises(ValueError, lambda: check_brackets('(')) + self.assertRaises(ValueError, lambda: check_brackets(')')) + self.assertRaises(ValueError, lambda: check_brackets('())(')) + self.assertRaises(ValueError, lambda: check_brackets('(()))')) + self.assertRaises(ValueError, lambda: check_brackets(')()(')) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(check_brackets('')) + self.assertIsNone(check_brackets('()')) + self.assertIsNone(check_brackets('((()))()')) + self.assertIsNone(check_brackets('()()()()')) + self.assertIsNone(check_brackets('(()(()())())')) + + def test_check_constant(self): + with self.subTest("Throws error if environment does not have constant"): + self.assertRaises(ValueError, lambda: check_constant('constant', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('constant + 5', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('sin(1) + constant + 7', self.lib)) + + with self.subTest("Throws error if constant name starts with digit"): + self.assertRaises(ValueError, lambda: check_constant('10constant', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('10constant + 5', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('sin(1) + 10constant + 7', self.lib)) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(check_constant('', self.lib)) + self.assertIsNone(check_constant('e', self.lib)) + self.assertIsNone(check_constant('sin(21)', self.lib)) + self.assertIsNone(check_constant('sin(21) + e', self.lib)) + self.assertIsNone(check_constant('2.4178516392292583e+24 + 5', self.lib)) + + def test_check_function(self): + with self.subTest("Throws error if environment does not have function"): + self.assertRaises(ValueError, lambda: check_function('multiply()', self.lib)) + self.assertRaises(ValueError, lambda: check_function('multiply(5,7)', self.lib)) + self.assertRaises(ValueError, lambda: check_function('multiply() + 7', self.lib)) + + with self.subTest("Throws error if function name starts with digit"): + self.assertRaises(ValueError, lambda: check_function('10log()', self.lib)) + self.assertRaises(ValueError, lambda: check_function('10log(1)', self.lib)) + self.assertRaises(ValueError, lambda: check_function('10log(5,7)', self.lib)) + self.assertRaises(ValueError, lambda: check_function('10log() + 7', self.lib)) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(check_function('', self.lib)) + self.assertIsNone(check_function('e', self.lib)) + self.assertIsNone(check_function('sin(21)', self.lib)) + self.assertIsNone(check_function('sin(21) + e', self.lib)) + self.assertIsNone(check_function('2.4178516392292583e+24 + 5', self.lib)) + + def test_check_expression(self): + with self.subTest("returns expression without spaces"): + self.assertEqual(check_expression('1 + 2', self.lib), '1+2') + self.assertEqual(check_expression('1-2', self.lib), '1-2') + self.assertEqual(check_expression('1 * - 2', self.lib), '1*-2') + self.assertEqual(check_expression('1 - sin (1, 2, 3) + - 2', self.lib), '1-sin(1,2,3)+-2') + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/converter/__init__.py b/final_task/calculator/converter/__init__.py new file mode 100644 index 00000000..78622ef8 --- /dev/null +++ b/final_task/calculator/converter/__init__.py @@ -0,0 +1 @@ +from .converter import convert_answer diff --git a/final_task/calculator/converter/converter.py b/final_task/calculator/converter/converter.py new file mode 100644 index 00000000..7ef04aa3 --- /dev/null +++ b/final_task/calculator/converter/converter.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to convert the expression to the desired type. + +Example: + convert_answer('-1', False) + >>> '-1' + + convert_answer('-1', False) + >>> '0' + + convert_answer('-1', True) + >>> 'True' + + convert_answer('0', True) + >>> 'False' +""" + +from ..regexp import has_non_zero_fraction_part + + +def convert_answer(expr: str, has_compare: bool) -> str: + """ + Converts the resulting string to the desired type. + + Args: + expr (str): String representation of a number. + has_compare (bool): whether the expression contains boolean logic + """ + num = float(expr) + match = has_non_zero_fraction_part(expr) + num = num if match else int(num) + + result = bool(num) if has_compare else num + + return str(result) diff --git a/final_task/calculator/converter/test_converter.py b/final_task/calculator/converter/test_converter.py new file mode 100644 index 00000000..e217834f --- /dev/null +++ b/final_task/calculator/converter/test_converter.py @@ -0,0 +1,15 @@ +import unittest +from .converter import convert_answer + + +class TestConverterFunction(unittest.TestCase): + def test_convert_answer(self): + with self.subTest("returns correct answer"): + self.assertEqual(convert_answer('-1', False), '-1') + self.assertEqual(convert_answer('0', False), '0') + self.assertEqual(convert_answer('-1', True), 'True') + self.assertEqual(convert_answer('0', True), 'False') + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/library/__init__.py b/final_task/calculator/library/__init__.py new file mode 100644 index 00000000..ff2f4e5f --- /dev/null +++ b/final_task/calculator/library/__init__.py @@ -0,0 +1 @@ +from .library import Library diff --git a/final_task/calculator/library/library.py b/final_task/calculator/library/library.py new file mode 100644 index 00000000..1255af76 --- /dev/null +++ b/final_task/calculator/library/library.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +The module is designed for dynamic creation of module libraries. + +Example: + lib = Library('math', 'os') + lib.update('time', 'os') + + lib['e'] + >>> 2.718281828459045 + + lib['sum'](5, 10) + >>> 15 +""" + + +class Library(dict): + """ + Class is designed to work with modules. + It is a dictionary of functions and constants. + """ + def __init__(self, *modules: list): + super().__init__() + + self['abs'] = abs + self['round'] = round + + self.update(*modules) + + def update(self, *modules: list): + """Adds functions and veriables from got module names to dictionary.""" + for module in modules: + super().update(__import__(module).__dict__) diff --git a/final_task/calculator/library/test_library.py b/final_task/calculator/library/test_library.py new file mode 100644 index 00000000..7eaedc86 --- /dev/null +++ b/final_task/calculator/library/test_library.py @@ -0,0 +1,38 @@ +import unittest +from .library import Library + + +class TestLibraryClass(unittest.TestCase): + def test__init__(self): + with self.subTest("contains around and abs functon by default"): + lib = Library() + + self.assertTrue('round' in lib) + self.assertTrue('abs' in lib) + + with self.subTest("can get module names and adds their variables to your own dictionary"): + lib = Library('math', 'os', 'time') + + self.assertTrue('path' in lib) + self.assertTrue('clock' in lib) + self.assertTrue('pi' in lib) + + def test_update(self): + with self.subTest("get module names and adds their variables to your own dictionary"): + lib = Library() + self.assertFalse('path' in lib) + + lib.update('os') + self.assertTrue('path' in lib) + + lib.update('sys', 'time') + self.assertTrue('stdin' in lib) + self.assertTrue('clock' in lib) + + with self.subTest("raises error if veriable is not found"): + self.assertRaises(ModuleNotFoundError, lambda: lib.update('bad_module')) + self.assertRaises(ModuleNotFoundError, lambda: lib.update('new_math')) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/operators/__init__.py b/final_task/calculator/operators/__init__.py new file mode 100644 index 00000000..c3797f53 --- /dev/null +++ b/final_task/calculator/operators/__init__.py @@ -0,0 +1,19 @@ +from .operators import ( + LEFT_BRACKET, + RIGHT_BRACKET, + MULTIPLE, + POWER, + TRUE_DIVISION, + FLOOR_DIVISION, + MODULE, + PLUS, + MINUS, + LESS, + LESS_OR_EQUAL, + GREAT, + GREAT_OR_EQUAL, + EQUAL, + NOT_EQUAL, + OPERATORS, + exec_operation, +) diff --git a/final_task/calculator/operators/operators.py b/final_task/calculator/operators/operators.py new file mode 100644 index 00000000..671f90d9 --- /dev/null +++ b/final_task/calculator/operators/operators.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to calculate mathematical operations. +Also contains string representations of operations. + +Attributes: + LEFT_BRACKET (str): possible representation of the bracket ( in the expression. + RIGHT_BRACKET (str): possible representation of the bracket ) in the expression. + MULTIPLE (str): possible representation of the operation * in the expression. + POWER (str): possible representation of the operation ** in the expression. + TRUE_DIVISION (str): possible representation of the operation / in the expression. + FLOOR_DIVISION (str): possible representation of the operation // in the expression. + MODULE (str): possible representation of the operation % in the expression. + PLUS (str): possible representation of the operation + in the expression. + MINUS (str): possible representation of the operation - in the expression. + LESS (str): possible representation of the operation < in the expression. + LESS_OR_EQUAL (str): possible representation of the operation <= in the expression. + GREAT (str): possible representation of the operation > in the expression. + GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. + EQUAL (str): possible representation of the operation == in the expression. + NOT_EQUAL (str): possible representation of the operation != in the expression. + OPERATORS (dict): key is string representation of operations, and value is namedtuple(func, type). +""" + +import re +from collections import namedtuple +from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt +from .types import ARITHMETIC, COMPARISON + + +LEFT_BRACKET = '(' +RIGHT_BRACKET = ')' +MULTIPLE = '*' +POWER = '^' +TRUE_DIVISION = '/' +FLOOR_DIVISION = '//' +MODULE = '%' +PLUS = '+' +MINUS = '-' +LESS = '<' +LESS_OR_EQUAL = '<=' +GREAT = '>' +GREAT_OR_EQUAL = '>=' +EQUAL = '==' +NOT_EQUAL = '!=' + + +Operator = namedtuple('Operator', 'func type') +OPERATORS = { + MULTIPLE: Operator(mul, ARITHMETIC), + POWER: Operator(pow, ARITHMETIC), + TRUE_DIVISION: Operator(truediv, ARITHMETIC), + FLOOR_DIVISION: Operator(floordiv, ARITHMETIC), + MODULE: Operator(mod, ARITHMETIC), + PLUS: Operator(add, ARITHMETIC), + MINUS: Operator(sub, ARITHMETIC), + LESS: Operator(lt, COMPARISON), + LESS_OR_EQUAL: Operator(le, COMPARISON), + EQUAL: Operator(eq, COMPARISON), + NOT_EQUAL: Operator(ne, COMPARISON), + GREAT_OR_EQUAL: Operator(ge, COMPARISON), + GREAT: Operator(gt, COMPARISON), +} + + +def exec_operation(val1: str, val2: str, operation=MULTIPLE) -> str: + """Executes the operation and returns the result. + + Args: + val1 (str): String representation of a number. + val2 (str): String representation of a number. + + Returns: + str: result of calculations. + + Raises: + ValueError: If `operation` is not found`. + """ + if operation not in OPERATORS: + raise ValueError('operation was not found') + + if operation == POWER and val2[0] == MINUS: + converted_val1, converted_val2 = float(val2[1:]), float(val1) + if operation == POWER: + converted_val1, converted_val2 = float(val2), float(val1) + else: + converted_val1, converted_val2 = float(val1), float(val2) + + operator = OPERATORS[operation] + result = operator.func(converted_val1, converted_val2) + + if operator.type == ARITHMETIC: + if operation == POWER and val2[0] == MINUS: + return f'{MINUS}{result}' + return f'{PLUS}{result}' if result > 0 else str(result) + + if operator.type == COMPARISON: + return str(int(result)) diff --git a/final_task/calculator/operators/test_operators.py b/final_task/calculator/operators/test_operators.py new file mode 100644 index 00000000..8576c025 --- /dev/null +++ b/final_task/calculator/operators/test_operators.py @@ -0,0 +1,48 @@ +import unittest +from .operators import ( + PLUS, + MINUS, + POWER, + MODULE, + MULTIPLE, + TRUE_DIVISION, + FLOOR_DIVISION, + LESS, + GREAT, + EQUAL, + NOT_EQUAL, + LESS_OR_EQUAL, + GREAT_OR_EQUAL, + exec_operation, +) + + +class TestOperatorFunction(unittest.TestCase): + def test_exec_operation(self): + a, b = '3', '7' + + with self.subTest("Arithmetic operations return currect sting value"): + self.assertEqual(exec_operation(a, b, MULTIPLE), '+21.0') + self.assertEqual(exec_operation(b, a, POWER), '+2187.0') + self.assertEqual(exec_operation(a, b, TRUE_DIVISION), '+0.42857142857142855') + self.assertEqual(exec_operation(a, b, FLOOR_DIVISION), '0.0') + self.assertEqual(exec_operation(a, b, MODULE), '+3.0') + self.assertEqual(exec_operation(a, b, PLUS), '+10.0') + self.assertEqual(exec_operation(a, b, MINUS), '-4.0') + + with self.subTest("Comparison operations return currect sting value 1 (True) or 0 (False)"): + self.assertEqual(float(exec_operation(a, b, LESS)), a < b) + self.assertEqual(float(exec_operation(a, b, LESS_OR_EQUAL)), a <= b) + self.assertEqual(float(exec_operation(a, b, EQUAL)), a == b) + self.assertEqual(float(exec_operation(a, b, NOT_EQUAL)), a != b) + self.assertEqual(float(exec_operation(a, b, GREAT_OR_EQUAL)), a >= b) + self.assertEqual(float(exec_operation(a, b, GREAT)), a > b) + + with self.subTest("If don't have operation throw error"): + self.assertRaises(ValueError, lambda: exec_operation(a, b, '**')) + self.assertRaises(ValueError, lambda: exec_operation(a, b, '&&')) + self.assertRaises(ValueError, lambda: exec_operation(a, b, '||')) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/operators/types/__init__.py b/final_task/calculator/operators/types/__init__.py new file mode 100644 index 00000000..ce587534 --- /dev/null +++ b/final_task/calculator/operators/types/__init__.py @@ -0,0 +1,4 @@ +from .types import ( + ARITHMETIC, + COMPARISON, +) diff --git a/final_task/calculator/operators/types/types.py b/final_task/calculator/operators/types/types.py new file mode 100644 index 00000000..2a443e19 --- /dev/null +++ b/final_task/calculator/operators/types/types.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +The module contains operation type constants. + +Attributes: + ARITHMETIC (int): constant type arithmetic operators. + COMPARISON (int): constant type comparison operators. +""" + +ARITHMETIC = 0 +COMPARISON = 1 diff --git a/final_task/calculator/parser/__init__.py b/final_task/calculator/parser/__init__.py new file mode 100644 index 00000000..f7cefe76 --- /dev/null +++ b/final_task/calculator/parser/__init__.py @@ -0,0 +1 @@ +from .parser import parse_query diff --git a/final_task/calculator/parser/parser.py b/final_task/calculator/parser/parser.py new file mode 100644 index 00000000..e199aecd --- /dev/null +++ b/final_task/calculator/parser/parser.py @@ -0,0 +1,21 @@ +from argparse import ArgumentParser, Namespace + + +def parse_query() -> Namespace: + """ + Convert argument strings to objects and assign them as attributes of the namespace. + + Returns: + Namespace: got data from command line. + """ + parser = ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('expr', metavar='EXPRESSION', help='expression string to evaluate') + parser.add_argument('-m', + '--use-modules', + default=[], + dest='modules', + metavar='MODULE', + nargs='+', + help='additional modules to use') + + return parser.parse_args() diff --git a/final_task/calculator/parser/test_parser.py b/final_task/calculator/parser/test_parser.py new file mode 100644 index 00000000..b820bf4a --- /dev/null +++ b/final_task/calculator/parser/test_parser.py @@ -0,0 +1,17 @@ +import unittest +from .parser import parse_query + + +class TestParserFunction(unittest.TestCase): + def test_parse_query(self): + with self.subTest("return currect value"): + import sys + + sys.argv = ['pycalc.py', 'time()/60', '-m', 'time', 'os', 'math'] + args = parse_query() + self.assertEqual(args.expr, 'time()/60') + self.assertEqual(args.modules, ['time', 'os', 'math']) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py new file mode 100755 index 00000000..b019c241 --- /dev/null +++ b/final_task/calculator/pycalc.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to work with mathematical expressions. + +Example: + $ python pycalc.py -h + $ python pycalc.py 'expretion' + $ python pycalc.py 'expretion' -m 'module1' 'module2' +""" + +import re +from .library import Library +from .parser import parse_query +from .checker import check_expression +from .replacer import replace_all_mathes +from .regexp import has_comparator +from .converter import convert_answer + + +def main(): + """ + Performs processing and calculation of the request + from the command line and displays it on the screen. + """ + try: + lib = Library('math') + args = parse_query() + lib.update(*args.modules) + expr = check_expression(args.expr, lib) + has_compare = has_comparator(expr) + result = replace_all_mathes(expr, lib) + result = convert_answer(result, has_compare) + print(result) + except Exception as e: + print(f'ERROR: {e}') + + +if __name__ == '__main__': + main() diff --git a/final_task/calculator/regexp/__init__.py b/final_task/calculator/regexp/__init__.py new file mode 100644 index 00000000..0e6b28d3 --- /dev/null +++ b/final_task/calculator/regexp/__init__.py @@ -0,0 +1,17 @@ +from .regexp import ( + REGEXP_DIGIT, + REGEXP_SIMPLE_DIGIT, + REGEXP_SCREENING, + REGEX_NAME, + REGEXP_BACKETS, + REGEXP_FUNCTION, + REGEXP_CONSTANT, + REGEXP_UNARY, + REGEXP_BYNARY, + REGEXP_COMPARE, + REGEXP_NON_ZERO_FRACTION_PART, + REGEXP_COMPARATOR, + REGEXP_INCORECT_EXPRETION, + has_comparator, + has_non_zero_fraction_part, +) diff --git a/final_task/calculator/regexp/regexp.py b/final_task/calculator/regexp/regexp.py new file mode 100644 index 00000000..28560173 --- /dev/null +++ b/final_task/calculator/regexp/regexp.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +The module is a regular expression library for searching math expressions. + +Example: + has_comparator('1==3') + >>> True + + has_comparator('1+2') + >>> False + + has_non_zero_fraction_part('1.0') + >>> False + + has_non_zero_fraction_part('1.01') + >>> True + +Attributes: + REGEXP_DIGIT (rstr): regular expressions for finding numbers. + REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. + REGEXP_SCREENING (rstr): regular expressions for operation screening. + REGEX_NAME (rstr): regular expressions for finding names. + REGEXP_BACKETS (rstr): regular expressions for finding brackets. + REGEXP_FUNCTION (rstr): regular expressions for finding functons. + REGEXP_CONSTANT (rstr): regular expressions for finding constant names. + REGEXP_UNARY (rstr): regular expressions for finding unary operation. + REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. + REGEXP_COMPARE (rstr): regular expressions for finding compare operation. + REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. + REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. + REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. +""" + +import re + +REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' +REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' +REGEXP_SCREENING = rf'\{{operation}}' +REGEX_NAME = r'\w+' +REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' +REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' +REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' +REGEXP_UNARY = rf'([-+]{{2,}})' +REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' +REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') +REGEXP_NON_ZERO_FRACTION_PART = r'\.0*[1-9]' +REGEXP_COMPARATOR = r'[=!<>]{1,2}' +REGEXP_INCORECT_EXPRETION = ( + r'.?\W\d+\s*\(|' + r'^\d+\s*\(|' + r'^\W*$|' + r'\d+[)(<=!>][<>!]\d+|' + r'\W\d+[)(<=!>][]\d+|' + r'\w+\s+\w+|' + r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' + r'^[\/*^%<=!>]|' + r'[-+*^\/%<=!>]$' +) + + +def has_comparator(expr: str) -> bool: + """ + Checks if expression has a comparator. + Returns True if the expression contains, otherwise False. + """ + match = re.search(REGEXP_COMPARATOR, expr) + return bool(match) + + +def has_non_zero_fraction_part(expr: str) -> bool: + """ + Checks if expression has a non zero fraction part + Returns True if the expression contains, otherwise False. + """ + match = re.search(REGEXP_NON_ZERO_FRACTION_PART, expr) + return bool(match) diff --git a/final_task/calculator/regexp/test_regexp.py b/final_task/calculator/regexp/test_regexp.py new file mode 100644 index 00000000..e0e74319 --- /dev/null +++ b/final_task/calculator/regexp/test_regexp.py @@ -0,0 +1,23 @@ +import unittest +from .regexp import has_comparator, has_non_zero_fraction_part + + +class TestRegexpFunction(unittest.TestCase): + def test_has_comparator(self): + with self.subTest("returns correct answer"): + self.assertFalse(has_comparator('1')) + self.assertFalse(has_comparator('1+1', )) + self.assertTrue(has_comparator('1==1')) + self.assertTrue(has_comparator('1>=1')) + + def test_has_non_zero_fraction_part(self): + with self.subTest("returns correct answer"): + self.assertFalse(has_non_zero_fraction_part('1')) + self.assertFalse(has_non_zero_fraction_part('1.0')) + self.assertTrue(has_non_zero_fraction_part('1.9')) + self.assertTrue(has_non_zero_fraction_part('1.09')) + self.assertTrue(has_non_zero_fraction_part('1.00000000000001')) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/replacer/__init__.py b/final_task/calculator/replacer/__init__.py new file mode 100644 index 00000000..a4f758c6 --- /dev/null +++ b/final_task/calculator/replacer/__init__.py @@ -0,0 +1,9 @@ +from .replacer import ( + replace_constant, + replace_fanction, + replace_unary_operator, + replace_compare_operator, + replace_bynary_operator, + replace_brackets, + replace_all_mathes, +) diff --git a/final_task/calculator/replacer/replacer.py b/final_task/calculator/replacer/replacer.py new file mode 100644 index 00000000..0c605e41 --- /dev/null +++ b/final_task/calculator/replacer/replacer.py @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +""" +The module is intended to replace mathematical expressions with their result. + +Example: + replace_unary_operator('1+-+-+-3') + >>> '1-3' + + lib = { + 'e': 2.718281828459045, + 'sum': sum + } + replace_constant('1+e', lib) + >>> '1+2.718281828459045' + + replace_fanction('sum(100,50)', lib) + >>> '150' +""" + +import re +from collections import namedtuple +from functools import reduce +from ..library import Library +from ..operators import ( + LEFT_BRACKET, + MINUS, + PLUS, + MULTIPLE, + POWER, + TRUE_DIVISION, + FLOOR_DIVISION, + MODULE, + EQUAL, + NOT_EQUAL, + GREAT, + GREAT_OR_EQUAL, + LESS, + LESS_OR_EQUAL, + exec_operation, +) +from ..regexp import ( + REGEXP_BACKETS, + REGEXP_CONSTANT, + REGEXP_DIGIT, + REGEXP_SCREENING, + REGEXP_BYNARY, + REGEXP_COMPARE, + REGEXP_UNARY, + REGEXP_FUNCTION, + REGEXP_SIMPLE_DIGIT, +) + + +def replace_constant(expr: str, library: Library) -> str: + """ + Calculates constant operations. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Returns: + str: Updated expression. + """ + matches = re.finditer(REGEXP_CONSTANT, expr) + + for match in matches: + name = match.group('name') + + if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): + continue + + result = str(library[name]) + arr = expr.split(name) + + for idx, piece in enumerate(arr[:-1]): + if piece and piece[-1].isalnum(): + arr[idx] = f'{piece}{name}' + elif piece or not idx: + arr[idx] = f'{piece}{result}' + + expr = ''.join(arr) + + return expr + + +def replace_fanction(expr: str, library: Library) -> str: + """ + Calculates function operations. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Returns: + str: Updated expression. + """ + matches = re.finditer(REGEXP_FUNCTION, expr) + + for match in matches: + func = match.group('name') + pattern = match.group('pattern') + args = filter(bool, match.group('args').split(',')) + args = [float(v) for v in args] + result = str(library[func](*args)) + expr = expr.replace(pattern, result) + + return expr + + +def replace_unary_operator(expr: str) -> str: + """ + Calculates unary operations. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + matches = re.findall(REGEXP_UNARY, expr) + matches.sort(key=len, reverse=True) + + for match in matches: + result = MINUS if match.count(MINUS) % 2 else PLUS + expr = expr.replace(match, result) + + return expr + + +def replace_compare_operator(expr: str, *operations: list) -> str: + """ + Calculates compare operations. + + Args: + expr (str): String mathematical expression. + *operations (list): List of operations that need to be done on the expression. + + Returns: + str: Updated expression. + """ + if re.search(REGEXP_COMPARE, expr): + return replace_bynary_operator(expr, *operations) + + return expr + + +def replace_bynary_operator(expr: str, *operations: list) -> str: + """ + Calculates binary operations. + + Args: + expr (str): String mathematical expression. + *operations (list): List of operations that need to be done on the expression. + + Returns: + str: Updated expression. + """ + for operation in operations: + delimeter = operation + if operation == PLUS or operation == MULTIPLE or operation == POWER: + delimeter = REGEXP_SCREENING.format(operation=operation) + + regexp = REGEXP_BYNARY.format(operation=delimeter) + matches = re.findall(regexp, expr) + for match in matches: + operands = list(filter(bool, match.split(operation))) + if operation == MINUS and match[0] == MINUS: + operands[0] = f'{MINUS}{operands[0]}' + if operation == POWER: + operands = operands[::-1] + + result = reduce(lambda acc, val: exec_operation(acc, val, operation=operation), operands) + expr = expr.replace(match, result) + + return expr + + +def replace_brackets(expr: str, library: Library) -> str: + """ + Calculates the expression in brackets. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Returns: + str: Updated expression. + """ + matches = re.findall(REGEXP_BACKETS, expr) + + for match in matches: + result = replace_all_mathes(match[1:-1], library) + expr = expr.replace(match, result) + + return expr + + +def replace_all_mathes(expr: str, library: Library) -> str: + """ + Calculates the result from the getting expression. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Returns: + str: result of calculations. + """ + Operation = namedtuple('Operation', 'func args') + OPERATION_PRIORITY = [ + Operation(replace_constant, [library]), + Operation(replace_fanction, [library]), + Operation(replace_brackets, [library]), + Operation(replace_unary_operator, []), + Operation(replace_bynary_operator, [POWER]), + Operation(replace_bynary_operator, [MULTIPLE, TRUE_DIVISION, FLOOR_DIVISION, MODULE]), + Operation(replace_bynary_operator, [PLUS, MINUS]), + Operation(replace_compare_operator, [EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL]), + ] + + pattern = re.compile(REGEXP_SIMPLE_DIGIT) + while True: + for operation in OPERATION_PRIORITY: + expr = operation.func(expr, *operation.args) + if pattern.match(expr): + return expr + + return expr diff --git a/final_task/calculator/replacer/test_replacer.py b/final_task/calculator/replacer/test_replacer.py new file mode 100644 index 00000000..10067e5e --- /dev/null +++ b/final_task/calculator/replacer/test_replacer.py @@ -0,0 +1,151 @@ +import unittest +from ..library import Library +from .replacer import ( + replace_constant, + replace_fanction, + replace_brackets, + replace_unary_operator, + replace_bynary_operator, + replace_compare_operator, + replace_all_mathes +) +from ..operators import ( + MULTIPLE, + POWER, + TRUE_DIVISION, + FLOOR_DIVISION, + MODULE, + PLUS, + MINUS, +) + + +class TestReplaceFunction(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib = Library('math', 'time') + + def test_replace_constant(self): + with self.subTest("Replaces constant name to constant value"): + self.assertEqual(replace_constant('e', self.lib), '2.718281828459045') + self.assertEqual(replace_constant('e + e', self.lib), '2.718281828459045 + 2.718281828459045') + + with self.subTest("Does not touch function and digit"): + self.assertEqual(replace_constant('log()', self.lib), 'log()') + self.assertEqual(replace_constant('log(e)', self.lib), 'log(2.718281828459045)') + self.assertEqual(replace_constant('log(e) + e', self.lib), 'log(2.718281828459045) + 2.718281828459045') + self.assertEqual(replace_constant('2.161727821137838e+16', self.lib), '2.161727821137838e+16') + self.assertEqual(replace_constant('time(e) + e', self.lib), 'time(2.718281828459045) + 2.718281828459045') + + def test_replace_fanction(self): + with self.subTest("Replaces function expression to function result"): + self.assertEqual(replace_fanction('log10(100)', self.lib), '2.0') + self.assertEqual(replace_fanction('log10(100) + log10(100)', self.lib), '2.0 + 2.0') + self.assertEqual(replace_fanction('log(100,10)', self.lib), '2.0') + + with self.subTest("Does not touch constants"): + self.assertEqual(replace_fanction('log10(100) + log(e)', self.lib), '2.0 + log(e)') + self.assertEqual(replace_fanction('log10(100) + e', self.lib), '2.0 + e') + self.assertEqual(replace_fanction('log10(e)', self.lib), 'log10(e)') + self.assertEqual(replace_fanction('log10(e) + 1', self.lib), 'log10(e) + 1') + + with self.subTest("Can receive seveeral arguments"): + self.assertEqual(replace_fanction('log(100,10)', self.lib), '2.0') + self.assertEqual(replace_fanction('hypot(-2,0)', self.lib), '2.0') + self.assertEqual(replace_fanction('hypot(-2,0) + log(100,10)', self.lib), '2.0 + 2.0') + + def test_replace_unary_operator(self): + with self.subTest("Replaces sequence of unary operators"): + self.assertEqual(replace_unary_operator('+---+1'), '-1') + self.assertEqual(replace_unary_operator('+--+1'), '+1') + self.assertEqual(replace_unary_operator('-13'), '-13') + self.assertEqual(replace_unary_operator('-+---+-1'), '-1') + + def test_replace_bynary_operator(self): + with self.subTest("Replaces sequence of bynary operators"): + self.assertEqual(float(replace_bynary_operator('1*2*3*4', MULTIPLE)), eval('1*2*3*4')) + self.assertEqual(float(replace_bynary_operator('2^3^4', POWER)), eval('2**3**4')) + self.assertEqual(float(replace_bynary_operator('1/2/3/4', TRUE_DIVISION)), eval('1/2/3/4')) + self.assertEqual(float(replace_bynary_operator('1//2//3', FLOOR_DIVISION)), eval('1//2//3')) + self.assertEqual(float(replace_bynary_operator('1%2%3%4', MODULE)), eval('1%2%3%4')) + self.assertEqual(float(replace_bynary_operator('1+2+3+4', PLUS)), eval('1+2+3+4')) + self.assertEqual(float(replace_bynary_operator('1-2-3-4', MINUS)), eval('1-2-3-4')) + self.assertEqual(float(replace_bynary_operator('-1-2-3-4', MINUS)), eval('-1-2-3-4')) + + with self.subTest("May receive several operators"): + val = '1*2*3+1+2+3' + self.assertEqual(float(replace_bynary_operator(val, MULTIPLE, PLUS)), eval(val)) + val = '-1-2-3-4+1+2+3+4' + self.assertEqual(float(replace_bynary_operator(val, MINUS, PLUS)), eval(val)) + + def test_replace_brackets(self): + with self.subTest("Replaces inner brackets to result"): + self.assertEqual(replace_brackets('(1*2*3*4)', self.lib), '+24.0') + self.assertEqual(replace_brackets('1+(2+3*2)*3', self.lib), '1++8.0*3') + self.assertEqual(replace_brackets('10*(2+1)', self.lib), '10*+3.0') + self.assertEqual(replace_brackets('(100)', self.lib), '100') + self.assertEqual(replace_brackets('(((100)))', self.lib), '((100))') + + with self.subTest("Does not touch function brakets"): + self.assertEqual(replace_brackets('log(1*2*3*4)', self.lib), 'log(1*2*3*4)') + self.assertEqual(replace_brackets('log((5+95),10)', self.lib), 'log(+100.0,10)') + + def test_replace_all_mathes(self): + with self.subTest("Calculates unary operations"): + self.assertEqual(replace_all_mathes('-13', self.lib), '-13') + self.assertEqual(replace_all_mathes('6-(-13)', self.lib), '+19.0') + self.assertEqual(replace_all_mathes('1---1', self.lib), '0.0') + self.assertEqual(replace_all_mathes('-+---+-1', self.lib), '-1') + + with self.subTest("Calculates priority operations"): + self.assertEqual(replace_all_mathes('1+2*2', self.lib), '+5.0') + self.assertEqual(replace_all_mathes('1+(2+3*2)*3', self.lib), '+25.0') + self.assertEqual(replace_all_mathes('10*(2+1)', self.lib), '+30.0') + self.assertEqual(replace_all_mathes('10^(2+1)', self.lib), '+1000.0') + self.assertEqual(replace_all_mathes('100/3^2', self.lib), '+11.11111111111111') + self.assertEqual(replace_all_mathes('100/3%2^2', self.lib), '+1.3333333333333357') + + with self.subTest("Calculates constants and functions"): + self.assertEqual(replace_all_mathes('pi+e', self.lib), '+5.859874482048838') + self.assertEqual(replace_all_mathes('log(e)', self.lib), '1.0') + self.assertEqual(replace_all_mathes('sin(pi/2)', self.lib), '1.0') + self.assertEqual(replace_all_mathes('log10(100)', self.lib), '2.0') + self.assertEqual(replace_all_mathes('sin(pi/2)*111*6', self.lib), '+666.0') + self.assertEqual(replace_all_mathes('2*sin(pi/2)', self.lib), '+2.0') + + with self.subTest("Calculates assotiacive operations"): + self.assertEqual(replace_all_mathes('102%12%7', self.lib), '+6.0') + self.assertEqual(replace_all_mathes('100/4/3', self.lib), '+8.333333333333334') + self.assertEqual(replace_all_mathes('2^3^4', self.lib), '+2.4178516392292583e+24') + + with self.subTest("Calculates comparation operations"): + self.assertEqual(replace_all_mathes('1+2*3==1+2*3', self.lib), '1') + self.assertEqual(replace_all_mathes('e^5>=e^5+1', self.lib), '0') + self.assertEqual(replace_all_mathes('1+2*4/3+1!=1+2*4/3+2', self.lib), '1') + + with self.subTest("Calculates common operations"): + self.assertEqual(replace_all_mathes('(100)', self.lib), '100') + self.assertEqual(replace_all_mathes('666', self.lib), '666') + self.assertEqual(replace_all_mathes('-.1', self.lib), '-.1') + self.assertEqual(replace_all_mathes('1/3', self.lib), '+0.3333333333333333') + self.assertEqual(replace_all_mathes('1.0/3.0', self.lib), '+0.3333333333333333') + self.assertEqual(replace_all_mathes('.1*2.0^56.0', self.lib), '+7205759403792794.0') + self.assertEqual(replace_all_mathes('e^34', self.lib), '+583461742527453.9') + self.assertEqual(replace_all_mathes('(2.0^(pi/pi+e/e+2.0^0.0))', self.lib), '+8.0') + self.assertEqual(replace_all_mathes('(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)', self.lib), '+2.0') + self.assertEqual(replace_all_mathes('sin(pi/2^1)+log(1*4+2^2+1,3^2)', self.lib), '+2.0') + self.assertEqual(replace_all_mathes('2.0^(2.0^2.0*2.0^2.0)', self.lib), '+65536.0') + + val = '10*e^0*log10(.4-5/-0.1-10)--abs(-53/10)+-5' + self.assertEqual(replace_all_mathes(val, self.lib), '+16.36381365110605') + + val = 'sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))' + self.assertEqual(replace_all_mathes(val, self.lib), '0.76638122986603') + + val = ('sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))' + '+cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)') + self.assertEqual(replace_all_mathes(val, self.lib), '0.5361064001012783') + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/setup.py b/final_task/setup.py index e69de29b..893d57fb 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,22 @@ +import os +from setuptools import setup, find_packages + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +setup( + name='pycalc', + version='1.0', + description='Pure-python command-line calculator.', + long_description=read('README.md'), + packages=find_packages(), + python_requires='>=3.6', + entry_points={ + "console_scripts": [ + "pycalc=calculator.pycalc:main", + ] + }, + platforms='any', +)