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/evaluator.py b/final_task/pycalc/evaluator.py new file mode 100644 index 0000000..79e53ad --- /dev/null +++ b/final_task/pycalc/evaluator.py @@ -0,0 +1,73 @@ +from pycalc.operators import Operator, Function, Constant +from pycalc.parser import Parser +from pycalc.importmodules import FunctionParser +from pycalc.validator import Validator +from inspect import getfullargspec + + +def infix_to_postfix(parsed_exp): + stack = [] + postfix_list = [] + for token in parsed_exp: + if isinstance(token, Operator) or isinstance(token, Function): + if token.name == '(': + stack.append(token) + elif token.name == ')': + while stack and stack[-1].name != '(': + postfix_list.append(stack.pop()) + if stack: + stack.pop() + else: + if not token.associativity == 1: + while stack and token.priority < stack[-1].priority: + postfix_list.append(stack.pop()) + else: + while stack and token.priority <= stack[-1].priority: + postfix_list.append(stack.pop()) + stack.append(token) + elif isinstance(token, Function): + stack.append(token) + elif isinstance(token, Constant): + postfix_list.append(token) + elif Parser.is_number(token): + postfix_list.append(token) + else: + raise ValueError(f'name {token} is not defined') + while stack: + postfix_list.append(stack.pop()) + return postfix_list + + +def calculate(exp): + stack = [] + parser = Parser() + parsed_exp = parser.parse_expression(exp) + polish = infix_to_postfix(parsed_exp) + if all(isinstance(token, Operator) for token in polish): + raise ValueError('not valid input') + for token in polish: + if isinstance(token, Operator) or isinstance(token, Function) or isinstance(token, Constant): + if isinstance(token, Function) and len(polish) == 1: + stack.append(token.func()) + elif isinstance(token, Function): + x = stack.pop() + if type(x) is list: + res = token.func(*x) + else: + res = token.func(*[x]) + stack.append(res) + elif isinstance(token, Constant): + stack.append(token.func) + elif not token.is_binary: + x = stack.pop() + stack.append(token.func(x)) + else: + try: + y, x = stack.pop(), stack.pop() + stack.append(token.func(x, y)) + except Exception as e: + raise ValueError(f' binary operation must have two operands {e}') + + else: + stack.append(float(token)) + return stack[0] diff --git a/final_task/pycalc/importmodules.py b/final_task/pycalc/importmodules.py new file mode 100644 index 0000000..450b205 --- /dev/null +++ b/final_task/pycalc/importmodules.py @@ -0,0 +1,27 @@ +import importlib +from pycalc.operators import Function, Constant + + +class FunctionParser: + functions_dict = {} + constants_dict = {} + + def __init__(self): + self.parse_modules(['math']) + self.functions_dict['pow'] = Function(object, 6, 1, True, pow) + self.functions_dict['abs'] = Function(object, 6, 1, True, abs) + self.functions_dict['round'] = Function(object, 6, 1, True, round) + + def parse_modules(self, modules): + ''' Method that parse module names array and add to dictionary their name as a key and + callable object as a value. + :param modules: Array of modules names. + ''' + for module in modules: + modul = importlib.import_module(module) + for object in vars(modul): + if object[0:2] != '__': + if isinstance(vars(modul)[object], (int, float, complex)): + self.constants_dict[object] = Constant(object, 6, 1, True, vars(modul)[object]) + else: + self.functions_dict[object] = Function(object, 6, 1, True, vars(modul)[object]) diff --git a/final_task/pycalc/operators.py b/final_task/pycalc/operators.py new file mode 100644 index 0000000..ecf8c91 --- /dev/null +++ b/final_task/pycalc/operators.py @@ -0,0 +1,47 @@ +class Operator: + def __init__(self, name, priority, associativity, is_binary, func): + self.priority = priority + self.associativity = associativity + self.func = func + self.name = name + self.is_binary = is_binary + + +class Function: + def __init__(self, name, priority, associativity, is_binary, func): + self.priority = priority + self.associativity = associativity + self.func = func + self.name = name + self.is_binary = is_binary + + +class Constant: + def __init__(self, name, priority, associativity, is_binary, func): + self.priority = priority + self.associativity = associativity + self.func = func + self.name = name + self.is_binary = is_binary + + +operators_dict = { + '>=': Operator('>=', 0, 1, True, lambda x, y: x >= y), + '<=': Operator('<=', 0, 1, True, lambda x, y: x <= y), + '==': Operator('==', 0, 1, True, lambda x, y: x == y), + '!=': Operator('!=', 0, 1, True, lambda x, y: x != y), + '>': Operator('>', 0, 1, True, lambda x, y: x > y), + '<': Operator('<', 0, 1, True, lambda x, y: x >= y), + ',': Operator(',', 1, 1, True, lambda x, y: [x, y]), + '+': Operator('+', 2, 1, True, lambda x, y: x+y), + '-': Operator('-', 2, 1, True, lambda x, y: x-y), + ')': Operator(')', -1, 1, False, None), + '(': Operator('(', -1, 1, False, None), + '*': Operator('*', 3, 1, True, lambda x, y: x*y), + '/': Operator('/', 3, 1, True, lambda x, y: x/y), + '%': Operator('%', 3, 1, True, lambda x, y: x % y), + '//': Operator('//', 3, 1, True, lambda x, y: x // y), + 'unary_minus': Operator('unary_minus', 5, 1, False, lambda x: -x), + 'unary_plus': Operator('unary_plus', 5, 1, False, lambda x: x), + '^': Operator('^', 4, 2, True, lambda x, y: x**y), +} diff --git a/final_task/pycalc/parser.py b/final_task/pycalc/parser.py new file mode 100644 index 0000000..476a0c8 --- /dev/null +++ b/final_task/pycalc/parser.py @@ -0,0 +1,115 @@ +from pycalc.operators import operators_dict, Operator, Function, Constant +from pycalc.validator import Validator +from pycalc.importmodules import FunctionParser +functions_dict = {} +const_dict = {} + + +class Parser: + def __init__(self): + self.func_parser = FunctionParser() + + @staticmethod + def is_number(s): + """ Returns True is string is a number. """ + if isinstance(s, Operator) or isinstance(s, Function) or isinstance(s, Constant) or s == 'pow': + return False + return s.replace('.', '', 1).isdigit() + + @staticmethod + def is_operator(s): + return s in operators_dict + + @staticmethod + def is_function(s): + return s in FunctionParser.functions_dict + + @staticmethod + def is_constant(s): + return s in FunctionParser.constants_dict + + @staticmethod + def add_multiply_sign(lexem_list): + for i in range(1, len(lexem_list)): + if isinstance(lexem_list[i], Function) and not isinstance(lexem_list[i-1], Operator): + lexem_list.insert(i, operators_dict['*']) + elif (isinstance(lexem_list[i], Function) and isinstance(lexem_list[i-1], Operator) and + lexem_list[i-1].name == ')'): + lexem_list.insert(i, operators_dict['*']) + elif isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and \ + (isinstance(lexem_list[i-1], Constant) or + Parser.is_number(lexem_list[i-1])): + lexem_list.insert(i, operators_dict['*']) + elif (isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and + isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')'): + lexem_list.insert(i, operators_dict['*']) + elif (isinstance(lexem_list[i], Operator) and lexem_list[i].name == '(' and + not isinstance(lexem_list[i-1], Operator) and not isinstance(lexem_list[i-1], Function)): + lexem_list.insert(i, operators_dict['*']) + elif isinstance(lexem_list[i], Constant) and isinstance(lexem_list[i-1], Constant): + lexem_list.insert(i, operators_dict['*']) + return lexem_list + + def parse_expression(self, exp): + exp = Validator.pre_tokinaze(exp) + exp.replace(" ", "") + lexem_array = [] + start_index = 0 + end_index = len(exp) + while start_index != len(exp): + substring = exp[start_index:end_index] + if Parser.is_number(substring): + lexem_array.append(substring) + start_index, end_index = end_index, len(exp) + elif Parser.is_operator(substring): + operator = operators_dict[substring] + lexem_array.append(operator) + + start_index, end_index = end_index, len(exp) + elif Parser.is_constant(substring): + lexem_array.append(self.func_parser.constants_dict[substring]) + start_index, end_index = end_index, len(exp) + elif Parser.is_function(substring): + lexem_array.append(self.func_parser.functions_dict[substring]) + start_index, end_index = end_index, len(exp) + else: + end_index -= 1 + lex_list = Parser.add_multiply_sign(lexem_array) + unary_signs = Parser.find_unary_signs(lex_list) + final_lexem_list = Parser.remove_redundant_unary_signs(unary_signs, lex_list) + + return final_lexem_list + + @staticmethod + def find_unary_signs(lexem_list): + final_list = [] + for i in range(len(lexem_list)): + if i == 0 and isinstance(lexem_list[i], Operator) and lexem_list[i].name in ['+', '-']: + final_list.append(0) + elif ((isinstance(lexem_list[i], Operator) and lexem_list[i].name in ['+', '-']) and not + (isinstance(lexem_list[i-1], Constant) or Parser.is_number(lexem_list[i-1])) and not + (isinstance(lexem_list[i-1], Operator) and lexem_list[i-1].name == ')')): + final_list.append(i) + lexems_with_indicies = enumerate(lexem_list) + lexems_filter = list(filter(lambda x: x[0] in final_list, lexems_with_indicies)) + for unary_sign in lexems_filter: + if unary_sign[1] == '+': + lexem_list[unary_sign[0]] = operators_dict['unary_plus'] + else: + lexem_list[unary_sign[0]] = operators_dict['unary_minus'] + return lexems_filter + + @staticmethod + def remove_redundant_unary_signs(lexems_with_indicies, lex_list): + final_index = len(lexems_with_indicies)-1 + while final_index != -1: + last_index, last_sign = lexems_with_indicies[final_index] + prev_index, prev_sign = lexems_with_indicies[final_index - 1] + if last_index - 1 == prev_index and last_sign.name == prev_sign.name: + lex_list[prev_index:last_index + 1] = [operators_dict['unary_plus']] + lexems_with_indicies[final_index - 1: final_index + 1] = [(prev_index, operators_dict['+'])] + elif last_index - 1 == prev_index and last_sign != prev_sign: + lex_list[prev_index: last_index + 1] = [operators_dict['unary_minus']] + lexems_with_indicies[final_index - 1: final_index + 1] = [(prev_index, operators_dict['-'])] + final_index -= 1 + return lex_list diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py new file mode 100644 index 0000000..af70295 --- /dev/null +++ b/final_task/pycalc/pycalc.py @@ -0,0 +1,30 @@ +import argparse +import sys +from pycalc.importmodules import FunctionParser +from pycalc.evaluator import calculate + + +def get_args(): + '''This function parses and return arguments passed in''' + parser = argparse.ArgumentParser( + description='Script retrieves schedules from a given server') + parser.add_argument( + 'expression', help='') + + parser.add_argument( + '-m', '--use-modules', nargs='+', help='', required=False) + return parser.parse_args() + + +def main(): + try: + args = get_args() + parser = FunctionParser() + if args.use_modules: + parser.parse_modules(args.use_modules) + parser.parse_modules(['time']) + result = calculate(args.expression) + print(f'{result}') + except Exception as e: + print(f"ERROR: {e}") + sys.exit(1) diff --git a/final_task/pycalc/validator.py b/final_task/pycalc/validator.py new file mode 100644 index 0000000..9b731b4 --- /dev/null +++ b/final_task/pycalc/validator.py @@ -0,0 +1,63 @@ +import re + + +class Validator: + sign_arr = ['<', '>', '=', '!', '/'] + + @staticmethod + def normalize_string(str): + ''' Method that normalize string with expression. If we have more than one space between symbol, + it change multiply spaces with one space. + :param str: String with a math expression. + :return : Normalized string with a math expression. + ''' + return re.sub(r'\s+', ' ', str).strip() + + @staticmethod + def pre_tokinaze(str): + ''' Method that do a number of operations before tokenization. + :param str: String with a math expression. + :return : Amended string with a math expression. + ''' + str.lower() + if Validator.par_check(str): + normalize_str = Validator.normalize_string(str) + valid_string = Validator.validate_string(normalize_str).replace(" ", "") + return valid_string + else: + raise ValueError('Brackets not balanced') + + @staticmethod + def par_check(expression): + ''' Method that check for validity of brackets. + :param expression: String with math expression. + :return : True or False, depends on validity of brackets of a given expression. + ''' + mapping = dict(zip('({[', ')}]')) + queue = [] + for letter in expression: + if letter in mapping: + queue.append(mapping[letter]) + elif letter not in mapping.values(): + continue + elif not (queue and letter == queue.pop()): + return False + return not queue + + @staticmethod + def validate_string(str): + ''' Method that raise error if string with a math expression is not valid. + :param str: String with a math expression. + :return : string with a math expression if it is valid. + ''' + indices = enumerate(str) + for i, char in indices: + if char in Validator.sign_arr: + if str[i + 1] == ' ' and str[i + 2] == '=': + raise ValueError('invalid syntax') + elif char == '/' and str[i + 1] == ' ' and str[i + 2] == '/': + raise ValueError('invalid syntax') + elif char.isdigit() and i != len(str) - 1: + if str[i + 1] == ' ' and str[i + 2].isdigit(): + raise ValueError('invalid syntax') + return str diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..8be2bb6 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + packages=find_packages(), + __version__='1.0', + entry_points={ + 'console_scripts': [ + 'pycalc = pycalc.pycalc:main' + ] + }, +) diff --git a/final_task/tests/__init__.py b/final_task/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/final_task/tests/unit_tests.py b/final_task/tests/unit_tests.py new file mode 100644 index 0000000..5b52e80 --- /dev/null +++ b/final_task/tests/unit_tests.py @@ -0,0 +1,81 @@ +import unittest +from pycalc.parser import Parser +from pycalc.validator import Validator +from pycalc.importmodules import FunctionParser +from pycalc.operators import operators_dict +from pycalc.evaluator import calculate + + +class TestLexerMethods(unittest.TestCase): + def setUp(self): + self.parser = Parser() + self.function_parser = FunctionParser() + self.operator = operators_dict['+'] + self.lexem_indicies = [(0, operators_dict['-']), (1, operators_dict['-']), (2, operators_dict['-'])] + self.lex_arr = [operators_dict['-'], operators_dict['-'], operators_dict['-'], '3'] + + def test_is_number_method(self): + self.assertEqual(Parser.is_number('3.03'), True) + self.assertEqual(Parser.is_number('3.o'), False) + self.assertEqual(Parser.is_number(self.operator), False) + + def test_is_operator_method(self): + self.assertEqual(Parser.is_operator('+'), True) + self.assertEqual(Parser.is_operator('12'), False) + + def test_is_function_method(self): + self.assertEqual(Parser.is_function('sin'), True) + self.assertEqual(Parser.is_function('sin1wq'), False) + + def test_is_constant_method(self): + self.assertEqual(Parser.is_constant('pi'), True) + self.assertEqual(Parser.is_constant('21'), False) + + def test_find_unary_signs_methods(self): + self.assertEqual(Parser.find_unary_signs([operators_dict['-'], operators_dict['-'], + operators_dict['-'], '3']), + [(0, operators_dict['-']), (1, operators_dict['-']), (2, operators_dict['-'])]) + self.assertEqual(Parser.find_unary_signs([operators_dict['('], operators_dict['-'], operators_dict['-'], + operators_dict['-'], '3', operators_dict[')']]), + [(1, operators_dict['-']), (2, operators_dict['-']), (3, operators_dict['-'])]) + + def test_add_multiply_sign(self): + self.assertEqual(Parser.add_multiply_sign(['4', self.function_parser.functions_dict['sin']]), + ['4', operators_dict['*'], self.function_parser.functions_dict['sin']]) + self.assertEqual(Parser.add_multiply_sign([operators_dict['('], '4', operators_dict[')'], + self.function_parser.functions_dict['sin']]), + [operators_dict['('], '4', operators_dict[')'], operators_dict['*'], + self.function_parser.functions_dict['sin']]) + self.assertEqual(Parser.add_multiply_sign([operators_dict['('], '4', operators_dict[')'], operators_dict['('], + '4', operators_dict[')']]), + [operators_dict['('], '4', operators_dict[')'], operators_dict['*'], operators_dict['('], '4', + operators_dict[')']]) + self.assertEqual(Parser.add_multiply_sign(['5', operators_dict['('], '4', operators_dict[')']]), + ['5', operators_dict['*'], operators_dict['('], '4', operators_dict[')']]) + + def test_par_checker(self): + self.assertFalse(Validator.par_check('(()')) + self.assertFalse(Validator.par_check('(()))')) + self.assertFalse(Validator.par_check('(()}')) + self.assertTrue(Validator.par_check('((((()))))')) + self.assertTrue(Validator.par_check('3')) + self.assertTrue(Validator.par_check('((1 + 2))')) + + def test_validation(self): + expressions = ["2 >= 4 5", "2 > = 45", "3 ! = 4", "2 < = 4", "2 = = 4 5"] + for expression in expressions: + with self.assertRaises(ValueError) as context_manager: + Validator.validate_string(expression) + self.assertIn("invalid syntax", str(context_manager.exception)) + self.assertEqual(Validator.validate_string("2 >= 45"), "2 >= 45") + + def test_calculate(self): + exp = '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(calculate(exp), 0.5361064001012783) + self.assertEqual(calculate('(3+(4*5)/10)+pow(3,2)'), 14.0) + + def test_parse_expression(self): + self.assertEqual(self.parser.parse_expression('sin(5)*3'), + [self.function_parser.functions_dict['sin'], operators_dict['('], '5', operators_dict[')'], + operators_dict['*'], '3'])