diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..a7b1ebfb --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +name = "pycalc" \ No newline at end of file diff --git a/__main__.py b/__main__.py new file mode 100644 index 00000000..03988707 --- /dev/null +++ b/__main__.py @@ -0,0 +1,3 @@ +import pycalc + +pycalc.main() \ No newline at end of file diff --git a/calc.py b/calc.py new file mode 100644 index 00000000..79679f45 --- /dev/null +++ b/calc.py @@ -0,0 +1,126 @@ +import re +import math +from inspect import signature +from inspect import getargs +from operators import * +from split import * + + +def tran_in_pol_not(inp): + """Перевод введённого математического выражения в обратную польскую нотацию.""" + out_stack = [] + slave_stack = [] + # парсинг выражения + split_list = split_string(inp, list_of_op) + for i, char in enumerate(split_list): + if char is '(': + slave_stack.append(char) + elif char is ')': + if '(' not in slave_stack: + print("ERROR: brackets are not balanced") + raise Exception + while slave_stack: + elem = slave_stack.pop() + if elem is not '(': + out_stack.append(elem) + else: + break + elif is_num(char): + out_stack.append(char) + elif char in list_of_op: + if len(split_list) == 1: + print("ERROR: it is essential to have at list one operand") + raise ArithmeticError + if char in unary_op: + inf_op = list(split_by_prefix(char, ['+', '-']))[0] + if i + 1 != len(split_list) and (i == 0 or is_num(split_list[i + 1])): + out_stack.extend([split_list[i + 1], 1]) + split_list[i + 1] = inf_op + else: + if i + 1 == len(split_list): + out_stack.extend([1, inf_op]) + for index in range(i + 1, len(split_list)): + if is_num(split_list[index]): + if index + 1 == len(split_list): + split_list.extend([inf_op, 1]) + break + split_list.insert(index, 1) + split_list.insert(index + 1, inf_op) + break + elif operators[char].type == 'inf': + '''if len(slave_stack) == 0: + slave_stack.append(char) + else:''' + if char == '-': + if i == 0: + out_stack.append(0) + elif i + 1 == len(split_list): + print("ERROR: Invalid expression: there is no 2nd operand after -.") + raise ArithmeticError + elif is_num(split_list[i + 1]): + if split_list[i - 1] == '(' and split_list[i + 2] != ')': + out_stack.append(0) + + while slave_stack: + item = slave_stack.pop() + if item in parentheses or (item in list_of_op and + operators[char].priority < operators[item].priority): + slave_stack.append(item) + break + else: + out_stack.append(item) + slave_stack.append(char) + else: + slave_stack.append(char) + + while len(slave_stack) > 0: + if '(' in slave_stack: + print("ERROR: brackets are not balanced") + raise Exception + out_stack.append(slave_stack.pop()) + return out_stack + + +def pols_not(exp1): + """Вычисление результата выражения по введённой польской нотации.""" + stack = [] + index = 0 + value = 0 + end = False + while not end: + item = exp1[index] + # добавить в стек прочитанное число, + # или результат операции + if is_num(item): + value = float(item) + stack.append(value) + else: + if item in operators: + foo = operators[item].func + elif item in math_func: + foo = math_func[item] + else: + try: + foo = getattr(math, item) + except Exception as inst: + print("Unexpected instance:", inst) + raise + # Evaluate a function according to the type an args or just set a value + if callable(foo): + try: + if item in list_of_op and operators[item].type == 'inf': + op1, op2 = stack.pop(), stack.pop() + value = foo(op2, op1) + else: + value = foo(stack.pop()) + # выполнить операцию по ключу С + except ArithmeticError as err: + print("Handling run-time error with evaluating a function or taking args: ", err) + raise + else: + value = foo + stack.append(value) + index += 1 + if index >= len(exp1): + end = True + return value diff --git a/final_task/pycalc/__init__.py b/final_task/pycalc/__init__.py new file mode 100644 index 00000000..a7b1ebfb --- /dev/null +++ b/final_task/pycalc/__init__.py @@ -0,0 +1 @@ +name = "pycalc" \ No newline at end of file diff --git a/final_task/pycalc/__main__.py b/final_task/pycalc/__main__.py new file mode 100644 index 00000000..03988707 --- /dev/null +++ b/final_task/pycalc/__main__.py @@ -0,0 +1,3 @@ +import pycalc + +pycalc.main() \ No newline at end of file diff --git a/final_task/pycalc/calc.py b/final_task/pycalc/calc.py new file mode 100644 index 00000000..79679f45 --- /dev/null +++ b/final_task/pycalc/calc.py @@ -0,0 +1,126 @@ +import re +import math +from inspect import signature +from inspect import getargs +from operators import * +from split import * + + +def tran_in_pol_not(inp): + """Перевод введённого математического выражения в обратную польскую нотацию.""" + out_stack = [] + slave_stack = [] + # парсинг выражения + split_list = split_string(inp, list_of_op) + for i, char in enumerate(split_list): + if char is '(': + slave_stack.append(char) + elif char is ')': + if '(' not in slave_stack: + print("ERROR: brackets are not balanced") + raise Exception + while slave_stack: + elem = slave_stack.pop() + if elem is not '(': + out_stack.append(elem) + else: + break + elif is_num(char): + out_stack.append(char) + elif char in list_of_op: + if len(split_list) == 1: + print("ERROR: it is essential to have at list one operand") + raise ArithmeticError + if char in unary_op: + inf_op = list(split_by_prefix(char, ['+', '-']))[0] + if i + 1 != len(split_list) and (i == 0 or is_num(split_list[i + 1])): + out_stack.extend([split_list[i + 1], 1]) + split_list[i + 1] = inf_op + else: + if i + 1 == len(split_list): + out_stack.extend([1, inf_op]) + for index in range(i + 1, len(split_list)): + if is_num(split_list[index]): + if index + 1 == len(split_list): + split_list.extend([inf_op, 1]) + break + split_list.insert(index, 1) + split_list.insert(index + 1, inf_op) + break + elif operators[char].type == 'inf': + '''if len(slave_stack) == 0: + slave_stack.append(char) + else:''' + if char == '-': + if i == 0: + out_stack.append(0) + elif i + 1 == len(split_list): + print("ERROR: Invalid expression: there is no 2nd operand after -.") + raise ArithmeticError + elif is_num(split_list[i + 1]): + if split_list[i - 1] == '(' and split_list[i + 2] != ')': + out_stack.append(0) + + while slave_stack: + item = slave_stack.pop() + if item in parentheses or (item in list_of_op and + operators[char].priority < operators[item].priority): + slave_stack.append(item) + break + else: + out_stack.append(item) + slave_stack.append(char) + else: + slave_stack.append(char) + + while len(slave_stack) > 0: + if '(' in slave_stack: + print("ERROR: brackets are not balanced") + raise Exception + out_stack.append(slave_stack.pop()) + return out_stack + + +def pols_not(exp1): + """Вычисление результата выражения по введённой польской нотации.""" + stack = [] + index = 0 + value = 0 + end = False + while not end: + item = exp1[index] + # добавить в стек прочитанное число, + # или результат операции + if is_num(item): + value = float(item) + stack.append(value) + else: + if item in operators: + foo = operators[item].func + elif item in math_func: + foo = math_func[item] + else: + try: + foo = getattr(math, item) + except Exception as inst: + print("Unexpected instance:", inst) + raise + # Evaluate a function according to the type an args or just set a value + if callable(foo): + try: + if item in list_of_op and operators[item].type == 'inf': + op1, op2 = stack.pop(), stack.pop() + value = foo(op2, op1) + else: + value = foo(stack.pop()) + # выполнить операцию по ключу С + except ArithmeticError as err: + print("Handling run-time error with evaluating a function or taking args: ", err) + raise + else: + value = foo + stack.append(value) + index += 1 + if index >= len(exp1): + end = True + return value diff --git a/final_task/pycalc/operators.py b/final_task/pycalc/operators.py new file mode 100644 index 00000000..54f4691e --- /dev/null +++ b/final_task/pycalc/operators.py @@ -0,0 +1,28 @@ +import math +from collections import namedtuple + + +Operator = namedtuple('Operator', ("priority", "type", "func")) +operators = { + '+': Operator(3, 'inf', lambda a, b: a + b), + '-': Operator(3, 'inf', lambda a, b: a - b), + '*': Operator(2, 'inf', lambda a, b: a * b), + '/': Operator(2, 'inf', lambda a, b: a / b), + '//': Operator(2, 'inf', lambda a, b: a // b), + '%': Operator(2, 'inf', lambda a, b: getattr(math, 'fmod')(a, b)), + '^': Operator(1, 'inf', lambda a, b: getattr(math, 'pow')(a, b)), + '<': Operator(4, 'inf', lambda a, b: a < b), + '<=': Operator(4, 'inf', lambda a, b: a <= b), + '==': Operator(5, 'inf', lambda a, b: a == b), + '!=': Operator(5, 'inf', lambda a, b: a != b), + '>=': Operator(4, 'inf', lambda a, b: a >= b), + '>': Operator(4, 'inf', lambda a, b: a > b), + '!': Operator(5, 'post', lambda a: getattr(math, 'factorial')(a)) + } + +math_func = {'abs': abs, + 'round': round} +[math_func.update({attr: getattr(math, attr)}) for attr in dir(math) if callable(getattr(math, attr))] +parentheses = {'(': 3, ')': 3} +unary_op = ['++', '--'] +list_of_op = unary_op + list(operators.keys()) + list(parentheses.keys()) diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py new file mode 100644 index 00000000..cf7e6f79 --- /dev/null +++ b/final_task/pycalc/pycalc.py @@ -0,0 +1,14 @@ +import argparse +import calc + + +def main(): + parser = argparse.ArgumentParser(description="Pure-python command-line calculator.") + parser.add_argument("EXPRESSION", help="Please, enter an expression for calculating", type=str) + args = parser.parse_args() + result = calc.pols_not(calc.tran_in_pol_not(args.EXPRESSION)) + print(result) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/final_task/pycalc/split.py b/final_task/pycalc/split.py new file mode 100644 index 00000000..8ec72afd --- /dev/null +++ b/final_task/pycalc/split.py @@ -0,0 +1,38 @@ +import re +import operators +from operators import * + + +def is_num(char): + try: + float(char) + return True + except ValueError: + return False + + +def split_string(inp, prefixes=list_of_op): + """Разбиение строки по операторам и операндам с помощью регулярных выражений""" + """String splitting by operators and operands using regular expressions""" + str_list = re.findall('(?:\d*\.\d+)|(?:\d+\.?)|[a-zA-Z\d]+|\W+', inp) + new_str = [] + for item in str_list: + if is_num(item) or item in list_of_op: + new_str.append(item) + else: + new_str.extend(split_by_prefix(item, prefixes)) + return [i.strip(' ') for i in new_str] + + +def split_by_prefix(string, prefixes): + """Разбиение строки по префиксам.""" + regex = re.compile('|'.join(map(re.escape, prefixes))) + while True: + match = regex.match(string) + if not match: + break + end = match.end() + yield string[:end] + string = string[end:] + if string: + yield string diff --git a/final_task/pycalc/test.py b/final_task/pycalc/test.py new file mode 100644 index 00000000..504dfb7b --- /dev/null +++ b/final_task/pycalc/test.py @@ -0,0 +1,95 @@ +import unittest +from operators import * +import split +import calc + + +class CalcSimpleActionsTests(unittest.TestCase): + + def test_Operator(self): + self.assertEqual(operators['+'].func(5, 9), 14) + self.assertEqual(operators['-'].func(11, 2), 9) + self.assertEqual(operators['*'].func(2, 5), 10) + self.assertEqual(operators['/'].func(8, 4), 2) + self.assertEqual(operators['//'].func(20, 3), 6) + self.assertEqual(operators['%'].func(6, 3), 0) + self.assertEqual(operators['^'].func(2, 3), 8) + + self.assertEqual(operators['<'].func(3, 4), True) + self.assertEqual(operators['<='].func(5, 5), True) + self.assertEqual(operators['=='].func(9, 9), True) + self.assertEqual(operators['!='].func(6, 7), True) + self.assertEqual(operators['>='].func(10, 9), True) + self.assertEqual(operators['>'].func(15, 1), True) + + def test_priority(self): + self.assertEqual(operators['+'].priority, 3) + self.assertEqual(operators['-'].priority, 3) + self.assertEqual(operators['*'].priority, 2) + self.assertEqual(operators['/'].priority, 2) + self.assertEqual(operators['//'].priority, 2) + self.assertEqual(operators['%'].priority, 2) + self.assertEqual(operators['^'].priority, 1) + + self.assertEqual(operators['<'].priority, 4) + self.assertEqual(operators['<='].priority, 4) + self.assertEqual(operators['=='].priority, 5) + self.assertEqual(operators['!='].priority, 5) + self.assertEqual(operators['>='].priority, 4) + self.assertEqual(operators['>'].priority, 4) + self.assertEqual(operators['!'].priority, 5) + + def test_type(self): + self.assertEqual(operators['+'].type, 'inf') + self.assertEqual(operators['-'].type, 'inf') + self.assertEqual(operators['!'].type, 'post') + + +class SplitTests(unittest.TestCase): + + def test_is_num(self): + self.assertTrue(split.is_num('11.0')) + self.assertTrue(split.is_num('19')) + self.assertFalse(split.is_num('a')) + + def test_split_string(self): + s = '5+6/3*7' + self.assertEqual(split.split_string(s), ['5', '+', '6', '/', '3', '*', '7']) + s = '10>=0' + self.assertEqual(split.split_string(s), ['10', '>=', '0']) + s = 'sin(10)' + self.assertEqual(split.split_string(s), ['sin', '(', '10', ')']) + s = 'log10(100)' + self.assertEqual(split.split_string(s), ['log10', '(', '100', ')']) + s = '5+sin(10+5)-10^3' + self.assertEqual(split.split_string(s), ['5', '+', 'sin', '(', '10', '+', '5', ')', '-', '10', '^', '3']) + + def test_split_by_prefix(self): + s = '>=+-' + self.assertEqual(list(split.split_by_prefix(s, ['+', '-', '>=', '<=', '!=', '=='])), ['>=', '+', '-']) + + +class PolishNotTests(unittest.TestCase): + + def test_tran_in_pol_not(self): + s = '10+5^2-sin(10)' + self.assertEqual(calc.tran_in_pol_not(s), ['10', '5', '2', '^', '+', '10', 'sin', '-']) + s = '5+10-60^2' + self.assertEqual(calc.tran_in_pol_not(s), ['5', '10', '+', '60', '2', '^', '-']) + s = '(2.0^(pi/pi+e/e+2.0^0.0))' + self.assertEqual(calc.tran_in_pol_not(s), ['2.0', 'pi', 'pi', '/', 'e', 'e', '/', '+', '2.0', + '0.0', '^', '+', '^']) + + def test_pols_not(self): + s = ('pi', '2', '/', 'sin', '111', '*', '6', '*') + self.assertEqual(calc.pols_not(s), 666) + s = ('1', '2', '3', '2', '*', '+', '3', '*', '+') + self.assertEqual(calc.pols_not(s), 25) + s = ('10', '5', '2', '^', '+', '10', 'sin', '-') + self.assertEqual(calc.pols_not(s), 35.54402111088937) + s = ('5', '10', '+', '60', '2', '^', '-') + self.assertEqual(calc.pols_not(s), -3585.0) + + +if __name__ == '__main__': + unittest.main() diff --git a/operators.py b/operators.py new file mode 100644 index 00000000..54f4691e --- /dev/null +++ b/operators.py @@ -0,0 +1,28 @@ +import math +from collections import namedtuple + + +Operator = namedtuple('Operator', ("priority", "type", "func")) +operators = { + '+': Operator(3, 'inf', lambda a, b: a + b), + '-': Operator(3, 'inf', lambda a, b: a - b), + '*': Operator(2, 'inf', lambda a, b: a * b), + '/': Operator(2, 'inf', lambda a, b: a / b), + '//': Operator(2, 'inf', lambda a, b: a // b), + '%': Operator(2, 'inf', lambda a, b: getattr(math, 'fmod')(a, b)), + '^': Operator(1, 'inf', lambda a, b: getattr(math, 'pow')(a, b)), + '<': Operator(4, 'inf', lambda a, b: a < b), + '<=': Operator(4, 'inf', lambda a, b: a <= b), + '==': Operator(5, 'inf', lambda a, b: a == b), + '!=': Operator(5, 'inf', lambda a, b: a != b), + '>=': Operator(4, 'inf', lambda a, b: a >= b), + '>': Operator(4, 'inf', lambda a, b: a > b), + '!': Operator(5, 'post', lambda a: getattr(math, 'factorial')(a)) + } + +math_func = {'abs': abs, + 'round': round} +[math_func.update({attr: getattr(math, attr)}) for attr in dir(math) if callable(getattr(math, attr))] +parentheses = {'(': 3, ')': 3} +unary_op = ['++', '--'] +list_of_op = unary_op + list(operators.keys()) + list(parentheses.keys()) diff --git a/pycalc.py b/pycalc.py new file mode 100644 index 00000000..cf7e6f79 --- /dev/null +++ b/pycalc.py @@ -0,0 +1,14 @@ +import argparse +import calc + + +def main(): + parser = argparse.ArgumentParser(description="Pure-python command-line calculator.") + parser.add_argument("EXPRESSION", help="Please, enter an expression for calculating", type=str) + args = parser.parse_args() + result = calc.pols_not(calc.tran_in_pol_not(args.EXPRESSION)) + print(result) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..69f44de4 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="pycalc", + version="0.0.1", + author="Alena Karaliova", + author_email="koroliovalena90@gmail.com", + description="Pure-python command-line calculator.", + packages=setuptools.find_packages(), + entry_points={'console_scripts': + ['pycalc=pycalc:main']}, + py_modules=['pycalc', 'calc', 'split', 'operators'] +) diff --git a/split.py b/split.py new file mode 100644 index 00000000..8ec72afd --- /dev/null +++ b/split.py @@ -0,0 +1,38 @@ +import re +import operators +from operators import * + + +def is_num(char): + try: + float(char) + return True + except ValueError: + return False + + +def split_string(inp, prefixes=list_of_op): + """Разбиение строки по операторам и операндам с помощью регулярных выражений""" + """String splitting by operators and operands using regular expressions""" + str_list = re.findall('(?:\d*\.\d+)|(?:\d+\.?)|[a-zA-Z\d]+|\W+', inp) + new_str = [] + for item in str_list: + if is_num(item) or item in list_of_op: + new_str.append(item) + else: + new_str.extend(split_by_prefix(item, prefixes)) + return [i.strip(' ') for i in new_str] + + +def split_by_prefix(string, prefixes): + """Разбиение строки по префиксам.""" + regex = re.compile('|'.join(map(re.escape, prefixes))) + while True: + match = regex.match(string) + if not match: + break + end = match.end() + yield string[:end] + string = string[end:] + if string: + yield string diff --git a/test.py b/test.py new file mode 100644 index 00000000..504dfb7b --- /dev/null +++ b/test.py @@ -0,0 +1,95 @@ +import unittest +from operators import * +import split +import calc + + +class CalcSimpleActionsTests(unittest.TestCase): + + def test_Operator(self): + self.assertEqual(operators['+'].func(5, 9), 14) + self.assertEqual(operators['-'].func(11, 2), 9) + self.assertEqual(operators['*'].func(2, 5), 10) + self.assertEqual(operators['/'].func(8, 4), 2) + self.assertEqual(operators['//'].func(20, 3), 6) + self.assertEqual(operators['%'].func(6, 3), 0) + self.assertEqual(operators['^'].func(2, 3), 8) + + self.assertEqual(operators['<'].func(3, 4), True) + self.assertEqual(operators['<='].func(5, 5), True) + self.assertEqual(operators['=='].func(9, 9), True) + self.assertEqual(operators['!='].func(6, 7), True) + self.assertEqual(operators['>='].func(10, 9), True) + self.assertEqual(operators['>'].func(15, 1), True) + + def test_priority(self): + self.assertEqual(operators['+'].priority, 3) + self.assertEqual(operators['-'].priority, 3) + self.assertEqual(operators['*'].priority, 2) + self.assertEqual(operators['/'].priority, 2) + self.assertEqual(operators['//'].priority, 2) + self.assertEqual(operators['%'].priority, 2) + self.assertEqual(operators['^'].priority, 1) + + self.assertEqual(operators['<'].priority, 4) + self.assertEqual(operators['<='].priority, 4) + self.assertEqual(operators['=='].priority, 5) + self.assertEqual(operators['!='].priority, 5) + self.assertEqual(operators['>='].priority, 4) + self.assertEqual(operators['>'].priority, 4) + self.assertEqual(operators['!'].priority, 5) + + def test_type(self): + self.assertEqual(operators['+'].type, 'inf') + self.assertEqual(operators['-'].type, 'inf') + self.assertEqual(operators['!'].type, 'post') + + +class SplitTests(unittest.TestCase): + + def test_is_num(self): + self.assertTrue(split.is_num('11.0')) + self.assertTrue(split.is_num('19')) + self.assertFalse(split.is_num('a')) + + def test_split_string(self): + s = '5+6/3*7' + self.assertEqual(split.split_string(s), ['5', '+', '6', '/', '3', '*', '7']) + s = '10>=0' + self.assertEqual(split.split_string(s), ['10', '>=', '0']) + s = 'sin(10)' + self.assertEqual(split.split_string(s), ['sin', '(', '10', ')']) + s = 'log10(100)' + self.assertEqual(split.split_string(s), ['log10', '(', '100', ')']) + s = '5+sin(10+5)-10^3' + self.assertEqual(split.split_string(s), ['5', '+', 'sin', '(', '10', '+', '5', ')', '-', '10', '^', '3']) + + def test_split_by_prefix(self): + s = '>=+-' + self.assertEqual(list(split.split_by_prefix(s, ['+', '-', '>=', '<=', '!=', '=='])), ['>=', '+', '-']) + + +class PolishNotTests(unittest.TestCase): + + def test_tran_in_pol_not(self): + s = '10+5^2-sin(10)' + self.assertEqual(calc.tran_in_pol_not(s), ['10', '5', '2', '^', '+', '10', 'sin', '-']) + s = '5+10-60^2' + self.assertEqual(calc.tran_in_pol_not(s), ['5', '10', '+', '60', '2', '^', '-']) + s = '(2.0^(pi/pi+e/e+2.0^0.0))' + self.assertEqual(calc.tran_in_pol_not(s), ['2.0', 'pi', 'pi', '/', 'e', 'e', '/', '+', '2.0', + '0.0', '^', '+', '^']) + + def test_pols_not(self): + s = ('pi', '2', '/', 'sin', '111', '*', '6', '*') + self.assertEqual(calc.pols_not(s), 666) + s = ('1', '2', '3', '2', '*', '+', '3', '*', '+') + self.assertEqual(calc.pols_not(s), 25) + s = ('10', '5', '2', '^', '+', '10', 'sin', '-') + self.assertEqual(calc.pols_not(s), 35.54402111088937) + s = ('5', '10', '+', '60', '2', '^', '-') + self.assertEqual(calc.pols_not(s), -3585.0) + + +if __name__ == '__main__': + unittest.main()