From 0c0fbd97414943aa5d83fe0b05f24165f2aa02a7 Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Sun, 9 Dec 2018 17:28:42 +0300 Subject: [PATCH 1/8] Initial commit --- final_task/pycalc/__init__.py | 0 final_task/pycalc/core.py | 298 ++++++++++++++++++++++++++++++++++ final_task/pycalc/test.py | 62 +++++++ final_task/setup.py | 15 ++ 4 files changed, 375 insertions(+) create mode 100644 final_task/pycalc/__init__.py create mode 100644 final_task/pycalc/core.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 0000000..e69de29 diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py new file mode 100644 index 0000000..9368529 --- /dev/null +++ b/final_task/pycalc/core.py @@ -0,0 +1,298 @@ +"""The script itself.""" + +import math +import argparse +import sys +import operator +import re +from collections import namedtuple + + +class ErrorOperands(Exception): + pass + + +class ErrorOperators(Exception): + pass + + +class ErrorWhitespace(Exception): + pass + + +class ErrorBracketsBalance(Exception): + pass + + +class ErrorUnexpectedComma(Exception): + pass + + +class ErrorEmptyExpression(Exception): + pass + + +class ErrorParse(Exception): + pass + + +class ErrorUnknownTokens(Exception): + pass + + +sys.tracebacklimit = 0 + +op = namedtuple('op', ['prec', 'func']) + +operators = { + '^': op(4, operator.pow), + '*': op(3, operator.mul), + '/': op(3, operator.truediv), + '//': op(3, operator.floordiv), + '%': op(3, operator.mod), + '+': op(2, operator.add), + '-': op(2, operator.sub) +} + +prefix = namedtuple('prefix', ['func', 'args']) + +prefixes = { + 'sin': prefix(math.sin, 1), + 'cos': prefix(math.cos, 1), + 'tan': prefix(math.tan, 1), + 'asin': prefix(math.asin, 1), + 'acos': prefix(math.acos, 1), + 'atan': prefix(math.atan, 1), + 'sqrt': prefix(math.sqrt, 1), + 'exp': prefix(math.exp, 1), + 'log': prefix(math.log, 2), + 'log1p': prefix(math.log1p, 1), + 'log10': prefix(math.log10, 1), + 'factorial': prefix(math.factorial, 1), + 'pow': prefix(math.pow, 2), + 'abs': prefix(abs, 1), + 'round': prefix(round, 1), + 'p': prefix(operator.pos, 1), + 'n': prefix(operator.neg, 1) +} + +constants = { + 'pi': math.pi, + 'e': math.e, + 'tau': math.tau +} + +comparators = { + '<': operator.lt, + '>': operator.gt, + '<=': operator.le, + '>=': operator.ge, + '==': operator.eq, + '!=': operator.ne +} + + +def isnumber(num): + """Return 'True' if num is a number and 'False' otherwise.""" + try: + float(num) + return True + except ValueError: + return False + + +def check_whitespace(expression): + """Check for 'bad' whitespace in a given expression.""" + expression = expression.strip() + token = re.search(r'[><=!]\s+=|\*\s+\*|\d\.?\s+\.?\d|\/\s+\/', expression) + if token is not None: + raise ErrorWhitespace("ERROR: 'bad' whitespace in the expression.") + + +def check_brackets(expression): + """Check whether brackets are balanced in the expression.""" + count = 0 + for ex in expression: + if ex == '(': + count += 1 + elif ex == ')': + count -= 1 + if count < 0: + raise ErrorBracketsBalance("ERROR: brackets are not balanced.") + if count > 0: + raise ErrorBracketsBalance("ERROR: brackets are not balanced.") + + +def check_commas(expression): + """Check for commas in unexpected places.""" + token = re.search(r'[^\)ieu\d]\,|\,[^\dpet\(]|^\,|\,$', expression) + if token is not None: + raise ErrorUnexpectedComma("ERROR: unexpected comma.") + + +def get_tokens(expression, input_queue=None): + """Recursively split expression string into tokens and store them in input_queue.""" + if expression is '': + raise ErrorEmptyExpression("ERROR: no expression provided.") + if input_queue is None: + expression = expression.strip().lower().replace(' ', '') + input_queue = [] + token = re.match(r'\)|\(|\d+\.?\d*|[-+*/,^]|\.\d+|\w+|\W+', expression) + try: + token.group() + except Exception: + raise ErrorParse("ERROR: couldn't parse this expression.") + input_queue.append(token.group()) + if len(token.group()) < len(expression): + return get_tokens(expression[token.end():], input_queue) + else: + for index, token in enumerate(input_queue): + if (input_queue[index-1] in operators or + input_queue[index-1] is 'p' or + input_queue[index-1] is 'n' or + input_queue[index-1] is '(' or + index is 0): + if token is '+': + input_queue[index] = 'p' + if token is '-': + input_queue[index] = 'n' + return input_queue + + +def infix_to_postfix(input_queue): + """Shunting-yard algorithm that returns input expression in postfix notation.""" + if input_queue[-1] in operators or input_queue[-1] in prefixes: + raise ErrorOperators("ERROR: trailing operators.") + output_queue = [] + operator_stack = [] + while len(input_queue): + token = input_queue[0] + if isnumber(token): + output_queue.append(float(input_queue.pop(0))) + elif token in constants: + output_queue.append(constants[token]) + input_queue.pop(0) + elif token in prefixes: + operator_stack.append(input_queue.pop(0)) + elif token is '(': + operator_stack.append(input_queue.pop(0)) + elif token is ',': + # since a comma can appear only after a logarithm, a check is implemented here + if 'log' not in operator_stack: + raise ErrorUnexpectedComma("ERROR: unexpected comma.") + try: + while operator_stack[-1] is not '(': + output_queue.append(operator_stack.pop()) + input_queue.pop(0) + except IndexError: + raise ErrorUnexpectedComma("ERROR: unexpected comma.") + elif token in operators: + while True: + if not operator_stack: + operator_stack.append(input_queue.pop(0)) + break + if ((operator_stack[-1] is not '(') + and (operator_stack[-1] in prefixes + or operators[token].prec < operators[operator_stack[-1]].prec + or (operators[operator_stack[-1]].prec == operators[token].prec + and token is not '^'))): + output_queue.append(operator_stack.pop()) + operator_stack.append(input_queue.pop(0)) + break + elif token is ')': + while operator_stack[-1] is not '(': + output_queue.append(operator_stack.pop()) + operator_stack.pop() + input_queue.pop(0) + else: + raise ErrorUnknownTokens("ERROR: unknown tokens.") + while len(operator_stack): + output_queue.append(operator_stack.pop()) + return output_queue + + +def split_comparison(expression): + """ + Split given expression into two to compare them. + When given a non-comparison statement, return it without modifying. + """ + check_whitespace(expression) + check_brackets(expression) + check_commas(expression) + expression.strip().replace(' ', '') + token = re.findall(r'==|>=|<=|>|<|!=', expression) + if len(token) > 1: + raise ErrorOperators("ERROR: more than one comparison operator.") + elif len(token) is 0: + return expression, None + else: + expressions = expression.split(token[0]) + return expressions, token[0] + + +def calculate(expression): + """Calculate a postfix notation expression.""" + stack = [] + while expression: + token = expression.pop(0) + if isnumber(token): + stack.append(token) + elif token in operators: + operand_2 = stack.pop() + operand_1 = stack.pop() + result = operators[token].func(operand_1, operand_2) + stack.append(result) + elif token in prefixes: + if prefixes[token].args is 2: + if len(stack) is 1: + operand = stack.pop() + result = prefixes[token].func(operand) + stack.append(result) + else: + operand_2 = stack.pop() + operand_1 = stack.pop() + result = prefixes[token].func(operand_1, operand_2) + stack.append(result) + else: + operand = stack.pop() + result = prefixes[token].func(operand) + stack.append(result) + if len(stack) is 1: + return stack[0] + else: + raise ErrorOperands("ERROR: wrong number of operands.") + + +def get_result(expression): + expression = get_tokens(expression) + expression = infix_to_postfix(expression) + try: + expression = calculate(expression) + except ZeroDivisionError as err: + print("ERROR: {err}.".format(err=err)) + return expression + + +def compare(expressions, comparator): + calculated_expressions = [get_result(expr) for expr in expressions] + return comparators[comparator](expressions[0], expressions[1]) + + +def main(): + parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('EXPRESSION', action="store", help="expression string to evaluate") + args = parser.parse_args() + if args.EXPRESSION is not None: + try: + expression = args.EXPRESSION + expressions, comparator = split_comparison(expression) + if not comparator: + print(get_result(expressions)) + else: + print(compare(expressions, comparator)) + except Exception as exception: + return exception + + +if __name__ == '__main__': + main() diff --git a/final_task/pycalc/test.py b/final_task/pycalc/test.py new file mode 100644 index 0000000..92f7642 --- /dev/null +++ b/final_task/pycalc/test.py @@ -0,0 +1,62 @@ +import unittest +import math +from core import * + +class TestCalc(unittest.TestCase): + def test_check_brackets(self): + with self.assertRaises(ErrorBracketsBalance): + check_brackets('())') + + + def test_check_whitespace(self): + with self.assertRaises(ErrorWhitespace): + check_whitespace('1 2') + + + def test_check_commas(self): + with self.assertRaises(ErrorUnexpectedComma): + check_commas(',') + + + def test_get_tokens(self): + self.assertEqual(get_tokens('1+2+3'), ['1','+','2','+','3']) + with self.assertRaises(ErrorEmptyExpression): + get_tokens('') + + + def test_infix_to_postfix(self): + self.assertEqual(infix_to_postfix(['1','+','2','+','3']), [1.0,2.0,'+',3.0,'+']) + self.assertEqual(infix_to_postfix(['1','+','2','^','3']), [1.0,2.0,3.0,'^','+']) + with self.assertRaises(ErrorOperators): + infix_to_postfix(['+']) + with self.assertRaises(ErrorUnexpectedComma): + infix_to_postfix(['(','12',',','12',')']) + with self.assertRaises(ErrorUnknownTokens): + infix_to_postfix(['asdf']) + + + def test_split_comparison(self): + self.assertEqual(split_comparison('1>=2'), (['1','2'],'>=')) + self.assertEqual(split_comparison('1'), ('1', None)) + with self.assertRaises(ErrorOperators): + split_comparison('1>=2>=3') + + + def test_calculate(self): + self.assertEqual(calculate([3.0, 2.0, '+']), 5.0) + self.assertEqual(calculate([2.0, 2.0, 2.0, '^', '^']), 16.0) + with self.assertRaises(ErrorOperands): + calculate([3.0, 2.0, 1.0, '+']) + + + def test_get_result(self): + self.assertEqual(get_result('1+4'), 5.0) + + + def test_compare(self): + self.assertTrue(compare(['2', '1'], '>=')) + self.assertFalse(compare(['1', '2'], '>=')) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..c8b73fa 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,15 @@ +"""pycalc script setup""" + +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + author='wenaught', + description='Command-line Python calculator.', + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'pycalc=pycalc.core:main', + ] + }) From e60e2b403a14e373f32baaceee573b95fcede6c6 Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Sun, 9 Dec 2018 19:41:08 +0300 Subject: [PATCH 2/8] Fixed precedence errors --- final_task/pycalc/core.py | 11 ++++++----- final_task/pycalc/test.py | 39 ++++++++++++++++----------------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index 9368529..f6e4a5a 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -177,8 +177,8 @@ def infix_to_postfix(input_queue): elif token is '(': operator_stack.append(input_queue.pop(0)) elif token is ',': - # since a comma can appear only after a logarithm, a check is implemented here - if 'log' not in operator_stack: + # since a comma can appear only after a logarithm or power, a check is implemented here + if 'log' not in operator_stack and 'pow' not in operator_stack: raise ErrorUnexpectedComma("ERROR: unexpected comma.") try: while operator_stack[-1] is not '(': @@ -197,8 +197,9 @@ def infix_to_postfix(input_queue): or (operators[operator_stack[-1]].prec == operators[token].prec and token is not '^'))): output_queue.append(operator_stack.pop()) - operator_stack.append(input_queue.pop(0)) - break + else: + break + operator_stack.append(input_queue.pop(0)) elif token is ')': while operator_stack[-1] is not '(': output_queue.append(operator_stack.pop()) @@ -218,8 +219,8 @@ def split_comparison(expression): """ check_whitespace(expression) check_brackets(expression) + expression = re.sub(r'\s', '', expression) check_commas(expression) - expression.strip().replace(' ', '') token = re.findall(r'==|>=|<=|>|<|!=', expression) if len(token) > 1: raise ErrorOperators("ERROR: more than one comparison operator.") diff --git a/final_task/pycalc/test.py b/final_task/pycalc/test.py index 92f7642..3ffd5cb 100644 --- a/final_task/pycalc/test.py +++ b/final_task/pycalc/test.py @@ -1,62 +1,55 @@ import unittest import math -from core import * +from pycalc.core import * + class TestCalc(unittest.TestCase): def test_check_brackets(self): with self.assertRaises(ErrorBracketsBalance): check_brackets('())') - - + def test_check_whitespace(self): with self.assertRaises(ErrorWhitespace): check_whitespace('1 2') - - + def test_check_commas(self): with self.assertRaises(ErrorUnexpectedComma): check_commas(',') - - + def test_get_tokens(self): - self.assertEqual(get_tokens('1+2+3'), ['1','+','2','+','3']) + self.assertEqual(get_tokens('1+2+3'), ['1', '+', '2', '+', '3']) with self.assertRaises(ErrorEmptyExpression): get_tokens('') - - + def test_infix_to_postfix(self): - self.assertEqual(infix_to_postfix(['1','+','2','+','3']), [1.0,2.0,'+',3.0,'+']) - self.assertEqual(infix_to_postfix(['1','+','2','^','3']), [1.0,2.0,3.0,'^','+']) + self.assertEqual(infix_to_postfix(['1', '+', '2', '+', '3']), [1.0, 2.0, '+', 3.0, '+']) + self.assertEqual(infix_to_postfix(['1', '+', '2', '^', '3']), [1.0, 2.0, 3.0, '^', '+']) with self.assertRaises(ErrorOperators): infix_to_postfix(['+']) with self.assertRaises(ErrorUnexpectedComma): - infix_to_postfix(['(','12',',','12',')']) + infix_to_postfix(['(', '12', ',', '12', ')']) with self.assertRaises(ErrorUnknownTokens): infix_to_postfix(['asdf']) - - + def test_split_comparison(self): - self.assertEqual(split_comparison('1>=2'), (['1','2'],'>=')) + self.assertEqual(split_comparison('1>=2'), (['1', '2'], '>=')) self.assertEqual(split_comparison('1'), ('1', None)) with self.assertRaises(ErrorOperators): split_comparison('1>=2>=3') - - + def test_calculate(self): self.assertEqual(calculate([3.0, 2.0, '+']), 5.0) self.assertEqual(calculate([2.0, 2.0, 2.0, '^', '^']), 16.0) with self.assertRaises(ErrorOperands): calculate([3.0, 2.0, 1.0, '+']) - - + def test_get_result(self): self.assertEqual(get_result('1+4'), 5.0) - - + def test_compare(self): self.assertTrue(compare(['2', '1'], '>=')) self.assertFalse(compare(['1', '2'], '>=')) - + if __name__ == '__main__': unittest.main() From 4d4d15cd2da46ec76dd26cd13b35334e668a761e Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Sun, 9 Dec 2018 19:49:25 +0300 Subject: [PATCH 3/8] Removed bugs from previous bug removal --- final_task/pycalc/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index f6e4a5a..1bfcc06 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -198,8 +198,8 @@ def infix_to_postfix(input_queue): and token is not '^'))): output_queue.append(operator_stack.pop()) else: + operator_stack.append(input_queue.pop(0)) break - operator_stack.append(input_queue.pop(0)) elif token is ')': while operator_stack[-1] is not '(': output_queue.append(operator_stack.pop()) From f427d7451436383cf834dc9783ed2f939b93a4d7 Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Sun, 9 Dec 2018 19:58:32 +0300 Subject: [PATCH 4/8] Code style cleanup; hopefully last commit for base functionality --- final_task/pycalc/core.py | 2 +- final_task/setup.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index 1bfcc06..401b9e7 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -292,7 +292,7 @@ def main(): else: print(compare(expressions, comparator)) except Exception as exception: - return exception + print(exception) if __name__ == '__main__': diff --git a/final_task/setup.py b/final_task/setup.py index c8b73fa..6123c48 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -3,13 +3,13 @@ from setuptools import setup, find_packages setup( - name='pycalc', - version='1.0', - author='wenaught', - description='Command-line Python calculator.', - packages=find_packages(), - entry_points={ - 'console_scripts': [ - 'pycalc=pycalc.core:main', - ] - }) + name='pycalc', + version='1.0', + author='wenaught', + description='Command-line Python calculator.', + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'pycalc=pycalc.core:main', + ] + }) From f634b99516036f8843144acc7ad1da5f074ded3b Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Fri, 26 Apr 2019 16:08:31 +0300 Subject: [PATCH 5/8] .gitignore update, minor fixes --- .gitignore | 5 +++-- final_task/pycalc/core.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index d7dc40d..cd01afa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,9 @@ __pycache__/ *.py[cod] *$py.class -# PyCharm files +# IDE files .idea/ +.vscode/ # C extensions *.so @@ -104,4 +105,4 @@ venv.bak/ /site # mypy -.mypy_cache/ +.mypy_cache/ \ No newline at end of file diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index 401b9e7..3cddef5 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -42,7 +42,7 @@ class ErrorUnknownTokens(Exception): sys.tracebacklimit = 0 -op = namedtuple('op', ['prec', 'func']) +op = namedtuple('op', ['prec', 'func']) operators = { '^': op(4, operator.pow), @@ -147,10 +147,10 @@ def get_tokens(expression, input_queue=None): return get_tokens(expression[token.end():], input_queue) else: for index, token in enumerate(input_queue): - if (input_queue[index-1] in operators or - input_queue[index-1] is 'p' or - input_queue[index-1] is 'n' or - input_queue[index-1] is '(' or + if (input_queue[index - 1] in operators or + input_queue[index - 1] is 'p' or + input_queue[index - 1] is 'n' or + input_queue[index - 1] is '(' or index is 0): if token is '+': input_queue[index] = 'p' From 99afc7bacda5edbef0bab58da9115946b2408f87 Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Fri, 26 Apr 2019 16:24:39 +0300 Subject: [PATCH 6/8] refactored function names --- final_task/pycalc/core.py | 64 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index 3cddef5..548be75 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -8,35 +8,35 @@ from collections import namedtuple -class ErrorOperands(Exception): +class OperandsError(Exception): pass -class ErrorOperators(Exception): +class OperatorsError(Exception): pass -class ErrorWhitespace(Exception): +class WhitespaceError(Exception): pass -class ErrorBracketsBalance(Exception): +class UnbalancedBracketsError(Exception): pass -class ErrorUnexpectedComma(Exception): +class UnexpectedCommaError(Exception): pass -class ErrorEmptyExpression(Exception): +class EmptyExpressionError(Exception): pass -class ErrorParse(Exception): +class ParseError(Exception): pass -class ErrorUnknownTokens(Exception): +class UnknownTokensError(Exception): pass @@ -93,7 +93,6 @@ class ErrorUnknownTokens(Exception): def isnumber(num): - """Return 'True' if num is a number and 'False' otherwise.""" try: float(num) return True @@ -102,38 +101,38 @@ def isnumber(num): def check_whitespace(expression): - """Check for 'bad' whitespace in a given expression.""" + """Check for whitespace breaking comparison operators and numbers in a given expression.""" expression = expression.strip() token = re.search(r'[><=!]\s+=|\*\s+\*|\d\.?\s+\.?\d|\/\s+\/', expression) if token is not None: - raise ErrorWhitespace("ERROR: 'bad' whitespace in the expression.") + raise WhitespaceError("ERROR: unexpected whitespace in the expression.") def check_brackets(expression): """Check whether brackets are balanced in the expression.""" - count = 0 - for ex in expression: - if ex == '(': - count += 1 - elif ex == ')': - count -= 1 - if count < 0: - raise ErrorBracketsBalance("ERROR: brackets are not balanced.") - if count > 0: - raise ErrorBracketsBalance("ERROR: brackets are not balanced.") + symbol_count = 0 + for symbol in expression: + if symbol == '(': + symbol_count += 1 + elif symbol == ')': + symbol_count -= 1 + if symbol_count < 0: + raise UnbalancedBracketsError("ERROR: brackets are not balanced.") + if symbol_count > 0: + raise UnbalancedBracketsError("ERROR: brackets are not balanced.") def check_commas(expression): """Check for commas in unexpected places.""" token = re.search(r'[^\)ieu\d]\,|\,[^\dpet\(]|^\,|\,$', expression) if token is not None: - raise ErrorUnexpectedComma("ERROR: unexpected comma.") + raise UnexpectedCommaError("ERROR: unexpected comma.") def get_tokens(expression, input_queue=None): """Recursively split expression string into tokens and store them in input_queue.""" if expression is '': - raise ErrorEmptyExpression("ERROR: no expression provided.") + raise EmptyExpressionError("ERROR: no expression provided.") if input_queue is None: expression = expression.strip().lower().replace(' ', '') input_queue = [] @@ -141,7 +140,7 @@ def get_tokens(expression, input_queue=None): try: token.group() except Exception: - raise ErrorParse("ERROR: couldn't parse this expression.") + raise ParseError("ERROR: couldn't parse this expression.") input_queue.append(token.group()) if len(token.group()) < len(expression): return get_tokens(expression[token.end():], input_queue) @@ -159,10 +158,9 @@ def get_tokens(expression, input_queue=None): return input_queue -def infix_to_postfix(input_queue): - """Shunting-yard algorithm that returns input expression in postfix notation.""" +def convert_infix_to_postfix(input_queue): if input_queue[-1] in operators or input_queue[-1] in prefixes: - raise ErrorOperators("ERROR: trailing operators.") + raise OperatorsError("ERROR: trailing operators.") output_queue = [] operator_stack = [] while len(input_queue): @@ -179,13 +177,13 @@ def infix_to_postfix(input_queue): elif token is ',': # since a comma can appear only after a logarithm or power, a check is implemented here if 'log' not in operator_stack and 'pow' not in operator_stack: - raise ErrorUnexpectedComma("ERROR: unexpected comma.") + raise UnexpectedCommaError("ERROR: unexpected comma.") try: while operator_stack[-1] is not '(': output_queue.append(operator_stack.pop()) input_queue.pop(0) except IndexError: - raise ErrorUnexpectedComma("ERROR: unexpected comma.") + raise UnexpectedCommaError("ERROR: unexpected comma.") elif token in operators: while True: if not operator_stack: @@ -206,7 +204,7 @@ def infix_to_postfix(input_queue): operator_stack.pop() input_queue.pop(0) else: - raise ErrorUnknownTokens("ERROR: unknown tokens.") + raise UnknownTokensError("ERROR: unknown tokens.") while len(operator_stack): output_queue.append(operator_stack.pop()) return output_queue @@ -223,7 +221,7 @@ def split_comparison(expression): check_commas(expression) token = re.findall(r'==|>=|<=|>|<|!=', expression) if len(token) > 1: - raise ErrorOperators("ERROR: more than one comparison operator.") + raise OperatorsError("ERROR: more than one comparison operator.") elif len(token) is 0: return expression, None else: @@ -261,12 +259,12 @@ def calculate(expression): if len(stack) is 1: return stack[0] else: - raise ErrorOperands("ERROR: wrong number of operands.") + raise OperandsError("ERROR: wrong number of operands.") def get_result(expression): expression = get_tokens(expression) - expression = infix_to_postfix(expression) + expression = convert_infix_to_postfix(expression) try: expression = calculate(expression) except ZeroDivisionError as err: From 063a913f48396af5bb85ab5fbed30a2debfd62f8 Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Mon, 29 Apr 2019 15:08:19 +0300 Subject: [PATCH 7/8] parameterized tests, more fixes to core --- final_task/pycalc/core.py | 3 ++ final_task/pycalc/test.py | 65 ++++++++++++++++++++++++++------------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index 548be75..8fe4a78 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -106,6 +106,7 @@ def check_whitespace(expression): token = re.search(r'[><=!]\s+=|\*\s+\*|\d\.?\s+\.?\d|\/\s+\/', expression) if token is not None: raise WhitespaceError("ERROR: unexpected whitespace in the expression.") + return None def check_brackets(expression): @@ -120,6 +121,7 @@ def check_brackets(expression): raise UnbalancedBracketsError("ERROR: brackets are not balanced.") if symbol_count > 0: raise UnbalancedBracketsError("ERROR: brackets are not balanced.") + return None def check_commas(expression): @@ -127,6 +129,7 @@ def check_commas(expression): token = re.search(r'[^\)ieu\d]\,|\,[^\dpet\(]|^\,|\,$', expression) if token is not None: raise UnexpectedCommaError("ERROR: unexpected comma.") + return None def get_tokens(expression, input_queue=None): diff --git a/final_task/pycalc/test.py b/final_task/pycalc/test.py index 3ffd5cb..d4ff26f 100644 --- a/final_task/pycalc/test.py +++ b/final_task/pycalc/test.py @@ -1,46 +1,67 @@ +from parameterized import parameterized import unittest import math from pycalc.core import * -class TestCalc(unittest.TestCase): - def test_check_brackets(self): - with self.assertRaises(ErrorBracketsBalance): - check_brackets('())') +class TestCheckers(unittest.TestCase): + @parameterized.expand([ + ('wrong side brackets', ')('), + ('extra opening bracket', '(()'), + ('extra closing bracket', '())'), + ('just a mess', '()())))((('), + ]) + def test_check_brackets(self, name, brackets): + self.assertEqual(check_brackets('()'), None) + with self.assertRaises(UnbalancedBracketsError): + check_brackets(brackets) - def test_check_whitespace(self): - with self.assertRaises(ErrorWhitespace): - check_whitespace('1 2') + @parameterized.expand([ + ('numbers', '12 34'), + ('numbers with dots', '12. 34'), + ('comparison operator', '< ='), + ('floor division', '/ /'), + ('power', '* *'), + ]) + def test_check_whitespace(self, name, whitespace): + with self.assertRaises(WhitespaceError): + check_whitespace(whitespace) - def test_check_commas(self): - with self.assertRaises(ErrorUnexpectedComma): - check_commas(',') + @parameterized.expand([ + ('function', 'sin(1, 2)'), + ('on its own', ','), + ]) + def test_check_commas(self, name, comma): + with self.assertRaises(UnexpectedCommaError): + check_commas(comma) + +class TestCoreFunctions(unittest.TestCase): def test_get_tokens(self): self.assertEqual(get_tokens('1+2+3'), ['1', '+', '2', '+', '3']) - with self.assertRaises(ErrorEmptyExpression): + with self.assertRaises(EmptyExpressionError): get_tokens('') - def test_infix_to_postfix(self): - self.assertEqual(infix_to_postfix(['1', '+', '2', '+', '3']), [1.0, 2.0, '+', 3.0, '+']) - self.assertEqual(infix_to_postfix(['1', '+', '2', '^', '3']), [1.0, 2.0, 3.0, '^', '+']) - with self.assertRaises(ErrorOperators): - infix_to_postfix(['+']) - with self.assertRaises(ErrorUnexpectedComma): - infix_to_postfix(['(', '12', ',', '12', ')']) - with self.assertRaises(ErrorUnknownTokens): - infix_to_postfix(['asdf']) + def test_convert_infix_to_postfix(self): + self.assertEqual(convert_infix_to_postfix(['1', '+', '2', '+', '3']), [1.0, 2.0, '+', 3.0, '+']) + self.assertEqual(convert_infix_to_postfix(['1', '+', '2', '^', '3']), [1.0, 2.0, 3.0, '^', '+']) + with self.assertRaises(OperatorsError): + convert_infix_to_postfix(['+']) + with self.assertRaises(UnexpectedCommaError): + convert_infix_to_postfix(['(', '12', ',', '12', ')']) + with self.assertRaises(UnknownTokensError): + convert_infix_to_postfix(['asdf']) def test_split_comparison(self): self.assertEqual(split_comparison('1>=2'), (['1', '2'], '>=')) self.assertEqual(split_comparison('1'), ('1', None)) - with self.assertRaises(ErrorOperators): + with self.assertRaises(OperatorsError): split_comparison('1>=2>=3') def test_calculate(self): self.assertEqual(calculate([3.0, 2.0, '+']), 5.0) self.assertEqual(calculate([2.0, 2.0, 2.0, '^', '^']), 16.0) - with self.assertRaises(ErrorOperands): + with self.assertRaises(OperandsError): calculate([3.0, 2.0, 1.0, '+']) def test_get_result(self): From 07c3ccf5dffd87a4aa480d896603767e21985074 Mon Sep 17 00:00:00 2001 From: Petr Liakhavets Date: Tue, 30 Apr 2019 12:46:58 +0300 Subject: [PATCH 8/8] excluded stmts from coverage, added test cases --- final_task/pycalc/core.py | 10 ++--- final_task/pycalc/test.py | 85 ++++++++++++++++++++++++++++----------- final_task/setup.py | 4 +- 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/final_task/pycalc/core.py b/final_task/pycalc/core.py index 8fe4a78..386eb24 100644 --- a/final_task/pycalc/core.py +++ b/final_task/pycalc/core.py @@ -218,10 +218,7 @@ def split_comparison(expression): Split given expression into two to compare them. When given a non-comparison statement, return it without modifying. """ - check_whitespace(expression) - check_brackets(expression) expression = re.sub(r'\s', '', expression) - check_commas(expression) token = re.findall(r'==|>=|<=|>|<|!=', expression) if len(token) > 1: raise OperatorsError("ERROR: more than one comparison operator.") @@ -280,14 +277,17 @@ def compare(expressions, comparator): return comparators[comparator](expressions[0], expressions[1]) -def main(): +def main(): # pragma: no cover parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') parser.add_argument('EXPRESSION', action="store", help="expression string to evaluate") args = parser.parse_args() if args.EXPRESSION is not None: try: expression = args.EXPRESSION + check_whitespace(expression) + check_brackets(expression) expressions, comparator = split_comparison(expression) + check_commas(expression) if not comparator: print(get_result(expressions)) else: @@ -296,5 +296,5 @@ def main(): print(exception) -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover main() diff --git a/final_task/pycalc/test.py b/final_task/pycalc/test.py index d4ff26f..450cb1e 100644 --- a/final_task/pycalc/test.py +++ b/final_task/pycalc/test.py @@ -5,14 +5,17 @@ class TestCheckers(unittest.TestCase): + def test_check_brackets(self): + self.assertEqual(check_brackets('()'), None) + self.assertEqual(check_brackets('()()'), None) + @parameterized.expand([ ('wrong side brackets', ')('), ('extra opening bracket', '(()'), ('extra closing bracket', '())'), ('just a mess', '()())))((('), ]) - def test_check_brackets(self, name, brackets): - self.assertEqual(check_brackets('()'), None) + def test_check_brackets_raises(self, name, brackets): with self.assertRaises(UnbalancedBracketsError): check_brackets(brackets) @@ -42,35 +45,71 @@ def test_get_tokens(self): with self.assertRaises(EmptyExpressionError): get_tokens('') - def test_convert_infix_to_postfix(self): - self.assertEqual(convert_infix_to_postfix(['1', '+', '2', '+', '3']), [1.0, 2.0, '+', 3.0, '+']) - self.assertEqual(convert_infix_to_postfix(['1', '+', '2', '^', '3']), [1.0, 2.0, 3.0, '^', '+']) - with self.assertRaises(OperatorsError): - convert_infix_to_postfix(['+']) - with self.assertRaises(UnexpectedCommaError): - convert_infix_to_postfix(['(', '12', ',', '12', ')']) - with self.assertRaises(UnknownTokensError): - convert_infix_to_postfix(['asdf']) + @parameterized.expand([ + ('left associativity', ['1', '+', '2', '+', '3'], [1.0, 2.0, '+', 3.0, '+']), + ('right associativity', ['1', '+', '2', '^', '3'], [1.0, 2.0, 3.0, '^', '+']) + ]) + def test_convert_infix_to_postfix(self, name, input, output): + self.assertEqual(convert_infix_to_postfix(input), output) + + @parameterized.expand([ + ('unexpected operator', OperatorsError, ['+']), + ('unexpected comma', UnexpectedCommaError, ['(', '12', ',', '12', ')']), + ('unknown token', UnknownTokensError, ['asdf']) + ]) + def test_convert_infix_to_postfix_raises(self, name, error, argument): + with self.assertRaises(error): + convert_infix_to_postfix(argument) + + @parameterized.expand([ + ('1>=2', (['1', '2'], '>=')), + ('1', ('1', None)), + ]) + def test_split_comparison(self, arguments, result): + self.assertEqual(split_comparison(arguments), result) - def test_split_comparison(self): - self.assertEqual(split_comparison('1>=2'), (['1', '2'], '>=')) - self.assertEqual(split_comparison('1'), ('1', None)) + def test_split_comparison_raises(self): with self.assertRaises(OperatorsError): split_comparison('1>=2>=3') - def test_calculate(self): - self.assertEqual(calculate([3.0, 2.0, '+']), 5.0) - self.assertEqual(calculate([2.0, 2.0, 2.0, '^', '^']), 16.0) + +class TestEndToEnd(unittest.TestCase): + @parameterized.expand([ + (calculate([3.0, 2.0, '+']), 5.0), + (calculate([2.0, 2.0, 2.0, '^', '^']), 16.0), + ]) + def test_calculate(self, function, result): + self.assertEqual(function, result) + + def test_calculate_raises(self): with self.assertRaises(OperandsError): calculate([3.0, 2.0, 1.0, '+']) - def test_get_result(self): - self.assertEqual(get_result('1+4'), 5.0) + @parameterized.expand([ + (compare(['2', '1'], '>='), ), + (compare(['1', '2'], '<='), ), + ]) + def test_compare_true(self, argument): + self.assertTrue(argument) + + @parameterized.expand([ + (compare(['1', '2'], '>='), ), + (compare(['2', '1'], '<='), ), + ]) + def test_compare_false(self, argument): + self.assertFalse(argument) - def test_compare(self): - self.assertTrue(compare(['2', '1'], '>=')) - self.assertFalse(compare(['1', '2'], '>=')) + @parameterized.expand([ + ("simple", "1+2", 3.0), + ("precedence", "1+2*3", 7.0), + ("brackets", "(1+2)*3", 9.0), + ("single argument function", "sin(0)", 0.0), + ("two argument function", "log(100, 10)", 2.0), + ("right associativity", "4^3^2", 262144.0), + ]) + def test_get_result(self, name, expression, result): + self.assertEqual(get_result(expression), result) -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover unittest.main() diff --git a/final_task/setup.py b/final_task/setup.py index 6123c48..d222b36 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,6 +1,6 @@ """pycalc script setup""" -from setuptools import setup, find_packages +from setuptools import setup, find_packages # pragma: no cover setup( name='pycalc', @@ -12,4 +12,4 @@ 'console_scripts': [ 'pycalc=pycalc.core:main', ] - }) + }) # pragma: no cover