From 28ec33bbb45169e7006d5b17a3265fad152b3aa8 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Tue, 4 Jun 2019 09:25:09 +0300 Subject: [PATCH 1/9] python calculator first edition Three main methods are realized: string parsing, translation into Polish notation and counting with Polish notation. --- calc.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ operators.py | 69 ++++++++++++++++++++++++++++++++++++ pycalc.py | 13 +++++++ setup.py | 12 +++++++ split.py | 39 +++++++++++++++++++++ test.py | 80 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 312 insertions(+) create mode 100644 calc.py create mode 100644 operators.py create mode 100644 pycalc.py create mode 100644 setup.py create mode 100644 split.py create mode 100644 test.py diff --git a/calc.py b/calc.py new file mode 100644 index 00000000..1e76bae1 --- /dev/null +++ b/calc.py @@ -0,0 +1,99 @@ +import re +import math +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 is_num(char) or char in post_func: + out_stack.append(char) + elif char in inf_func: + 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): + raise ArithmeticError + elif is_num(split_list[i + 1]): + if split_list[i - 1] == '(' and split_list[i + 2] != ')': + out_stack.append(0) + + while True and len(slave_stack) > 0: + item = slave_stack.pop() + if item not in inf_func or inf_func[char] < inf_func[item]: + slave_stack.append(item) + break + else: + out_stack.append(item) + slave_stack.append(char) + else: + if char is '(': + slave_stack.append(char) + elif char is ')': + while True: + elem = slave_stack.pop() + if elem is not '(': + out_stack.append(elem) + else: + break + else: + slave_stack.append(char) + + while len(slave_stack) > 0: + out_stack.append(slave_stack.pop()) + + return out_stack + + +def pols_not(exp1): + stack = [] + i = 0 + x = 0 + end = False + while not end: + c = exp1[i] + if is_num(c): + x = float(c) + stack.append(x) + else: + '''foo = None''' + if c in operators: + foo = operators[c] + else: + try: + foo = getattr(math, c) + except Exception as inst: + print("Unexpected error:", inst) + raise + #достать n последних числа из стека в зависимости от функции + '''params_count = len(signature(foo).parameters)#no signature found for builtin error in v3.6 + operands = [stack.pop() for _ in range(0, params_count)] + #выполнить операцию по ключу С''' + if callable(foo): + '''if params_count == 1: + x = foo(operands[0]) + elif params_count == 2: + x = foo(operands[1], operands[0]) + else: + x = foo(operands)''' + if c in inf_func: + op1, op2 = stack.pop(), stack.pop() + x = foo(op2, op1) + else: + x = foo(stack.pop()) + else: + #добавить в стек прочитанное число, + #или результат операции + x = foo + stack.append(x) + i += 1 + if i >= len(exp1): + end = True + return x + diff --git a/operators.py b/operators.py new file mode 100644 index 00000000..a90e1add --- /dev/null +++ b/operators.py @@ -0,0 +1,69 @@ +import math + + +class Operators: + + @staticmethod + def sum(a, b): + return a + b + + @staticmethod + def sub(a, b): + return a - b + + @staticmethod + def mul(a, b): + return a * b + + @staticmethod + def div(a, b): + return a / b + + @staticmethod + def fdiv(a, b): + return a // b + + @staticmethod + def less(a, b): + return a < b + + @staticmethod + def less_or_eql(a, b): + return a <= b + + @staticmethod + def eql(a, b): + return a == b + + @staticmethod + def not_eql(a, b): + return a != b + + @staticmethod + def gr_or_eql(a, b): + return a >= b + + @staticmethod + def greater(a, b): + return a > b + + +operators = {'+': Operators.sum, '-': Operators.sub, '*': Operators.mul, '/': Operators.div, + '//': Operators.fdiv, '%': getattr(math, 'fmod'), '^': getattr(math, 'pow'), '<': Operators.less, + '<=': Operators.less_or_eql, '==': Operators.eql, '!=': Operators.not_eql, + '>=': Operators.gr_or_eql, '>': Operators.greater, 'abs': abs, 'round': round} + +inf_func = {'^': 1, + '*': 2, '/': 2, '//': 2, '%': 2, + '+': 3, '-': 3, + '<=': 4, '>=': 4, '<': 4, '>': 4, + '<>': 5, '==': 5, '!=': 5} + +post_func = {'!': 5} +parentheses = {'(': 3, ')': 3} +list_of_op = list(inf_func.keys()) + list(post_func.keys()) + list(parentheses.keys()) + + + +if __name__ == "__main__": + Operators() diff --git a/pycalc.py b/pycalc.py new file mode 100644 index 00000000..cf37655f --- /dev/null +++ b/pycalc.py @@ -0,0 +1,13 @@ +import argparse +import calc + +def executor(): + parser = argparse.ArgumentParser(description="Pure-python command-line calculator.") + parser.add_argument("EXPRESSION", help="Please, enter an expression for calculating") + args = parser.parse_args() + result = calc.pols_not(calc.tran_in_pol_not(args.EXPRESSION)) + print(result) + + +if __name__ == '__main__': + executor() \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..5f04185b --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + author='Alena Karaliova', + author_email='koroliovalena90@gmail.com', + packages=find_packages(), + entry_points={'console_scripts': ['pycalc.cli:main']}, + description='Pure-python command-line calculator.', + py_modules=['pycalc', 'calc', 'split', 'operators'] +) diff --git a/split.py b/split.py new file mode 100644 index 00000000..e99b31ee --- /dev/null +++ b/split.py @@ -0,0 +1,39 @@ +import re +from operators import * + + +def is_num(char): + try: + float(char) + return True + except ValueError: + return False + +'''def is_num(char): + return char.isdigit() or try: float(char) return True excep''' + +def split_string(inp, prefixes=list_of_op): + """Разбиение строки по операторам и операндам с помощью регулярных выражений""" + + str_list = re.findall('[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 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..dd89ebcf --- /dev/null +++ b/test.py @@ -0,0 +1,80 @@ +import unittest +from operators import Operators +import split +import calc + + +class CalcSimpleActionsTests(unittest.TestCase): + + def test_sum(self): + self.assertEqual(Operators.sum(5, 9), 14) + + def test_sub(self): + self.assertEqual(Operators.sub(11, 2), 9) + + def test_mul(self): + self.assertEqual(Operators.mul(2, 5), 10) + + def test_div(self): + self.assertEqual(Operators.div(8, 4), 2) + + def test_fdiv(self): + self.assertEqual(Operators.fdiv(20, 3), 6) + + def test_less(self): + self.assertLess(3, 4) + + def test_less_or_eql(self): + self.assertLessEqual(5, 5) + + def test_eql(self): + self.assertAlmostEqual(9, 9) + + def test_not_eql(self): + self.assertNotAlmostEqual(6, 7) + + def test_gr_or_eql(self): + self.assertGreaterEqual(10, 9) + + def test_greater(self): + self.assertGreater(15, 1) + + +class CalcTestCase(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): + s1 = '5+6/3*7' + self.assertEqual(split.split_string(s1), ['5', '+', '6', '/', '3', '*', '7']) + s2 = '10>=0' + self.assertEqual(split.split_string(s2), ['10', '>=', '0']) + s3 = 'sin(10)' + self.assertEqual(split.split_string(s3), ['sin', '(', '10', ')']) + s4 = 'log10(100)' + self.assertEqual(split.split_string(s4), ['log10', '(', '100', ')']) + s5 = '5+sin(10+5)-10^3' + self.assertEqual(split.split_string(s5), ['5', '+', 'sin', '(', '10', '+', '5', ')', '-', '10', '^', '3']) + + def test_split_by_prefix(self): + s = '>=+-' + self.assertEqual(list(split.split_by_prefix(s, ['+', '-', '>=', '<=', '!=', '=='])), ['>=', '+', '-']) + + def test_tran_in_pol_not(self): + s = '10+5^2-sin(10)' + s2 = '5+10-60^2' + self.assertEqual(calc.tran_in_pol_not(s), ['10', '5', '2', '^', '+', '10', 'sin', '-']) + self.assertEqual(calc.tran_in_pol_not(s2), ['5', '10', '+', '60', '2', '^', '-']) + + def test_pols_not(self): + s1 = ('pi', '2', '/', 'sin', '111', '*', '6', '*') + self.assertEqual(calc.pols_not(s1), 666) + s2 = ('1', '2', '3', '2', '*', '+', '3', '*', '+') + self.assertEqual(calc.pols_not(s2), 25) + + +if __name__ == '__main__': + unittest.main() From 60fb0112b8ed449bb1d7f8a0e7eb192eea52e188 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Wed, 5 Jun 2019 10:26:25 +0300 Subject: [PATCH 2/9] Change data structure. Simplification of the code, the conclusion of the basic operators in the dictionary. --- operators.py | 92 ++++++++++++++-------------------------------------- 1 file changed, 25 insertions(+), 67 deletions(-) diff --git a/operators.py b/operators.py index a90e1add..3efc9af4 100644 --- a/operators.py +++ b/operators.py @@ -1,69 +1,27 @@ import math - - -class Operators: - - @staticmethod - def sum(a, b): - return a + b - - @staticmethod - def sub(a, b): - return a - b - - @staticmethod - def mul(a, b): - return a * b - - @staticmethod - def div(a, b): - return a / b - - @staticmethod - def fdiv(a, b): - return a // b - - @staticmethod - def less(a, b): - return a < b - - @staticmethod - def less_or_eql(a, b): - return a <= b - - @staticmethod - def eql(a, b): - return a == b - - @staticmethod - def not_eql(a, b): - return a != b - - @staticmethod - def gr_or_eql(a, b): - return a >= b - - @staticmethod - def greater(a, b): - return a > b - - -operators = {'+': Operators.sum, '-': Operators.sub, '*': Operators.mul, '/': Operators.div, - '//': Operators.fdiv, '%': getattr(math, 'fmod'), '^': getattr(math, 'pow'), '<': Operators.less, - '<=': Operators.less_or_eql, '==': Operators.eql, '!=': Operators.not_eql, - '>=': Operators.gr_or_eql, '>': Operators.greater, 'abs': abs, 'round': round} - -inf_func = {'^': 1, - '*': 2, '/': 2, '//': 2, '%': 2, - '+': 3, '-': 3, - '<=': 4, '>=': 4, '<': 4, '>': 4, - '<>': 5, '==': 5, '!=': 5} - -post_func = {'!': 5} +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} -list_of_op = list(inf_func.keys()) + list(post_func.keys()) + list(parentheses.keys()) - - - -if __name__ == "__main__": - Operators() +list_of_op = list(operators.keys()) + list(parentheses.keys()) From 6334d1f98c99bb9d6c2445196439348f850228e2 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Wed, 5 Jun 2019 23:59:03 +0300 Subject: [PATCH 3/9] Split refactoring Refinement of standard strings using regular expressions. --- split.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/split.py b/split.py index e99b31ee..6235a6c2 100644 --- a/split.py +++ b/split.py @@ -1,4 +1,5 @@ import re +import operators from operators import * @@ -9,23 +10,21 @@ def is_num(char): except ValueError: return False -'''def is_num(char): - return char.isdigit() or try: float(char) return True excep''' def split_string(inp, prefixes=list_of_op): - """Разбиение строки по операторам и операндам с помощью регулярных выражений""" - - str_list = re.findall('[a-zA-Z\d]+|\W+', inp) + """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 new_str + return [i.strip(' ') for i in new_str] def split_by_prefix(string, prefixes): + """Split strings by prefixes.""" regex = re.compile('|'.join(map(re.escape, prefixes))) while True: match = regex.match(string) @@ -37,3 +36,4 @@ def split_by_prefix(string, prefixes): if string: yield string + From ffe14a1514d3d41252c005c95718c951fce83db2 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Thu, 6 Jun 2019 00:13:41 +0300 Subject: [PATCH 4/9] Refinement tests. Test structure is simplified. Added tests for defining the priority of operations and types, as well as the conversion into and out of Polish notation. --- test.py | 119 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/test.py b/test.py index dd89ebcf..504dfb7b 100644 --- a/test.py +++ b/test.py @@ -1,46 +1,51 @@ import unittest -from operators import Operators +from operators import * import split import calc class CalcSimpleActionsTests(unittest.TestCase): - def test_sum(self): - self.assertEqual(Operators.sum(5, 9), 14) - - def test_sub(self): - self.assertEqual(Operators.sub(11, 2), 9) - - def test_mul(self): - self.assertEqual(Operators.mul(2, 5), 10) - - def test_div(self): - self.assertEqual(Operators.div(8, 4), 2) - - def test_fdiv(self): - self.assertEqual(Operators.fdiv(20, 3), 6) - - def test_less(self): - self.assertLess(3, 4) - - def test_less_or_eql(self): - self.assertLessEqual(5, 5) - - def test_eql(self): - self.assertAlmostEqual(9, 9) - - def test_not_eql(self): - self.assertNotAlmostEqual(6, 7) - - def test_gr_or_eql(self): - self.assertGreaterEqual(10, 9) - - def test_greater(self): - self.assertGreater(15, 1) - - -class CalcTestCase(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')) @@ -48,32 +53,42 @@ def test_is_num(self): self.assertFalse(split.is_num('a')) def test_split_string(self): - s1 = '5+6/3*7' - self.assertEqual(split.split_string(s1), ['5', '+', '6', '/', '3', '*', '7']) - s2 = '10>=0' - self.assertEqual(split.split_string(s2), ['10', '>=', '0']) - s3 = 'sin(10)' - self.assertEqual(split.split_string(s3), ['sin', '(', '10', ')']) - s4 = 'log10(100)' - self.assertEqual(split.split_string(s4), ['log10', '(', '100', ')']) - s5 = '5+sin(10+5)-10^3' - self.assertEqual(split.split_string(s5), ['5', '+', 'sin', '(', '10', '+', '5', ')', '-', '10', '^', '3']) + 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)' - s2 = '5+10-60^2' self.assertEqual(calc.tran_in_pol_not(s), ['10', '5', '2', '^', '+', '10', 'sin', '-']) - self.assertEqual(calc.tran_in_pol_not(s2), ['5', '10', '+', '60', '2', '^', '-']) + 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): - s1 = ('pi', '2', '/', 'sin', '111', '*', '6', '*') - self.assertEqual(calc.pols_not(s1), 666) - s2 = ('1', '2', '3', '2', '*', '+', '3', '*', '+') - self.assertEqual(calc.pols_not(s2), 25) + 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__': From 968d2a959e40e9671d9e3da3d8b7a8a81c675763 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Thu, 6 Jun 2019 00:20:10 +0300 Subject: [PATCH 5/9] Update setup.py --- setup.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 5f04185b..69f44de4 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,16 @@ -from setuptools import setup, find_packages +import setuptools -setup( - name='pycalc', - version='1.0', - author='Alena Karaliova', - author_email='koroliovalena90@gmail.com', - packages=find_packages(), - entry_points={'console_scripts': ['pycalc.cli:main']}, - description='Pure-python command-line calculator.', - py_modules=['pycalc', 'calc', 'split', 'operators'] +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'] ) From befd18fd07f3b7cada7c59fd450723a98eed3576 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Thu, 6 Jun 2019 01:29:28 +0300 Subject: [PATCH 6/9] added unary operators --- operators.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/operators.py b/operators.py index 3efc9af4..54f4691e 100644 --- a/operators.py +++ b/operators.py @@ -24,4 +24,5 @@ 'round': round} [math_func.update({attr: getattr(math, attr)}) for attr in dir(math) if callable(getattr(math, attr))] parentheses = {'(': 3, ')': 3} -list_of_op = list(operators.keys()) + list(parentheses.keys()) +unary_op = ['++', '--'] +list_of_op = unary_op + list(operators.keys()) + list(parentheses.keys()) From 46ea91d67576a199524741a18d082dcc45f56d08 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Thu, 6 Jun 2019 01:46:48 +0300 Subject: [PATCH 7/9] refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refinement of the transformation of the expression into the Polish notation, упдатед намес of some variables, the addition of packages concerning unary operators, some exceptions were stated. --- calc.py | 158 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 93 insertions(+), 65 deletions(-) diff --git a/calc.py b/calc.py index 1e76bae1..00dea7f0 100644 --- a/calc.py +++ b/calc.py @@ -1,99 +1,127 @@ import re import math +from inspect import signature +from inspect import getargs from operators import * from split import * def tran_in_pol_not(inp): + """Translation of the entered mathematical expression in the reverse Polish notation.""" out_stack = [] slave_stack = [] + # expression parsing split_list = split_string(inp, list_of_op) for i, char in enumerate(split_list): - if is_num(char) or char in post_func: + 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 inf_func: - 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): - raise ArithmeticError - elif is_num(split_list[i + 1]): - if split_list[i - 1] == '(' and split_list[i + 2] != ')': + 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.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 True and len(slave_stack) > 0: - item = slave_stack.pop() - if item not in inf_func or inf_func[char] < inf_func[item]: - slave_stack.append(item) - break - else: - out_stack.append(item) - slave_stack.append(char) + 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: - if char is '(': - slave_stack.append(char) - elif char is ')': - while True: - elem = slave_stack.pop() - if elem is not '(': - out_stack.append(elem) - else: - break - else: - slave_stack.append(char) + 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): + """Calculation of the result of the expression for the entered Polish notation.""" stack = [] - i = 0 - x = 0 + index = 0 + value = 0 end = False while not end: - c = exp1[i] - if is_num(c): - x = float(c) - stack.append(x) + item = exp1[index] + # add the read number to the stack, + # or the result of the operation + if is_num(item): + value = float(item) + stack.append(value) else: - '''foo = None''' - if c in operators: - foo = operators[c] + if item in operators: + foo = operators[item].func + elif item in math_func: + foo = math_func[item] else: try: - foo = getattr(math, c) + foo = getattr(math, item) except Exception as inst: - print("Unexpected error:", inst) + print("Unexpected instance:", inst) raise - #достать n последних числа из стека в зависимости от функции - '''params_count = len(signature(foo).parameters)#no signature found for builtin error in v3.6 - operands = [stack.pop() for _ in range(0, params_count)] - #выполнить операцию по ключу С''' + # Evaluate a function according to the type an args or just set a value if callable(foo): - '''if params_count == 1: - x = foo(operands[0]) - elif params_count == 2: - x = foo(operands[1], operands[0]) - else: - x = foo(operands)''' - if c in inf_func: - op1, op2 = stack.pop(), stack.pop() - x = foo(op2, op1) - else: - x = foo(stack.pop()) + 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: - #добавить в стек прочитанное число, - #или результат операции - x = foo - stack.append(x) - i += 1 - if i >= len(exp1): + value = foo + stack.append(value) + index += 1 + if index >= len(exp1): end = True - return x + return value + From f096dc921bfea6afd3a02b2c53cb0e2958b2e826 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Thu, 6 Jun 2019 23:58:18 +0300 Subject: [PATCH 8/9] Final refactoring --- __init__.py | 1 + __main__.py | 3 +++ calc.py | 53 ++++++++++++++++++++++++++--------------------------- pycalc.py | 7 ++++--- split.py | 5 ++--- 5 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 __init__.py create mode 100644 __main__.py 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 index 00dea7f0..79679f45 100644 --- a/calc.py +++ b/calc.py @@ -7,10 +7,10 @@ def tran_in_pol_not(inp): - """Translation of the entered mathematical expression in the reverse Polish notation.""" + """Перевод введённого математического выражения в обратную польскую нотацию.""" out_stack = [] slave_stack = [] - # expression parsing + # парсинг выражения split_list = split_string(inp, list_of_op) for i, char in enumerate(split_list): if char is '(': @@ -32,7 +32,7 @@ def tran_in_pol_not(inp): print("ERROR: it is essential to have at list one operand") raise ArithmeticError if char in unary_op: - inf_op = list(split.split_by_prefix(char, ['+', '-']))[0] + 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 @@ -48,28 +48,28 @@ def tran_in_pol_not(inp): split_list.insert(index + 1, inf_op) break elif operators[char].type == 'inf': - if len(slave_stack) == 0: + '''if len(slave_stack) == 0: slave_stack.append(char) - else: - if char == '-': - if i == 0: + 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) - 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) + 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) @@ -82,15 +82,15 @@ def tran_in_pol_not(inp): def pols_not(exp1): - """Calculation of the result of the expression for the entered Polish notation.""" + """Вычисление результата выражения по введённой польской нотации.""" stack = [] index = 0 value = 0 end = False while not end: item = exp1[index] - # add the read number to the stack, - # or the result of the operation + # добавить в стек прочитанное число, + # или результат операции if is_num(item): value = float(item) stack.append(value) @@ -113,6 +113,7 @@ def pols_not(exp1): 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 @@ -123,5 +124,3 @@ def pols_not(exp1): if index >= len(exp1): end = True return value - - diff --git a/pycalc.py b/pycalc.py index cf37655f..cf7e6f79 100644 --- a/pycalc.py +++ b/pycalc.py @@ -1,13 +1,14 @@ import argparse import calc -def executor(): + +def main(): parser = argparse.ArgumentParser(description="Pure-python command-line calculator.") - parser.add_argument("EXPRESSION", help="Please, enter an expression for calculating") + 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__': - executor() \ No newline at end of file + main() \ No newline at end of file diff --git a/split.py b/split.py index 6235a6c2..8ec72afd 100644 --- a/split.py +++ b/split.py @@ -12,6 +12,7 @@ def is_num(char): 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 = [] @@ -24,7 +25,7 @@ def split_string(inp, prefixes=list_of_op): def split_by_prefix(string, prefixes): - """Split strings by prefixes.""" + """Разбиение строки по префиксам.""" regex = re.compile('|'.join(map(re.escape, prefixes))) while True: match = regex.match(string) @@ -35,5 +36,3 @@ def split_by_prefix(string, prefixes): string = string[end:] if string: yield string - - From 0e44af0fdbe88dadaf714bc968d4de4a97591de4 Mon Sep 17 00:00:00 2001 From: Alena Koroliova <47770796+9ElenaKoroliova@users.noreply.github.com> Date: Fri, 7 Jun 2019 00:09:02 +0300 Subject: [PATCH 9/9] Add files via upload --- final_task/pycalc/__init__.py | 1 + final_task/pycalc/__main__.py | 3 + final_task/pycalc/calc.py | 126 +++++++++++++++++++++++++++++++++ final_task/pycalc/operators.py | 28 ++++++++ final_task/pycalc/pycalc.py | 14 ++++ final_task/pycalc/split.py | 38 ++++++++++ final_task/pycalc/test.py | 95 +++++++++++++++++++++++++ 7 files changed, 305 insertions(+) create mode 100644 final_task/pycalc/__init__.py create mode 100644 final_task/pycalc/__main__.py create mode 100644 final_task/pycalc/calc.py create mode 100644 final_task/pycalc/operators.py create mode 100644 final_task/pycalc/pycalc.py create mode 100644 final_task/pycalc/split.py create mode 100644 final_task/pycalc/test.py 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()