diff --git a/final_task/pycalc b/final_task/pycalc new file mode 100755 index 0000000..893c0a4 --- /dev/null +++ b/final_task/pycalc @@ -0,0 +1,22 @@ +#!/usr/local/bin/python3 + +"""Pure-python command-line calculator.""" + +import argparse + +from pycalc_src import Calculator + + +def main(): + """Function parse argument and calculate expression.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('EXPRESSION', help='expression string to evaluate') + args = parser.parse_args() + + pycalc = Calculator(args.EXPRESSION) + print(pycalc.calculate()) + + +if __name__ == '__main__': + main() diff --git a/final_task/pycalc_src/__init__.py b/final_task/pycalc_src/__init__.py new file mode 100644 index 0000000..e3fcb49 --- /dev/null +++ b/final_task/pycalc_src/__init__.py @@ -0,0 +1 @@ +from pycalc_src.calculator import Calculator diff --git a/final_task/pycalc_src/calculator.py b/final_task/pycalc_src/calculator.py new file mode 100644 index 0000000..c088a28 --- /dev/null +++ b/final_task/pycalc_src/calculator.py @@ -0,0 +1,269 @@ +"""Calculator module.""" + +from pycalc_src.exceptions import CalculatorError + +from pycalc_src.operators import OPERATORS +from pycalc_src.operators import CONSTANTS +from pycalc_src.operators import UNARY_OPERATORS +from pycalc_src.operators import COMPARISON_SYMBOLS + +from pycalc_src.preprocessor import Preprocessor + + +class Calculator: + """Calculator object.""" + + def __init__(self, expression): + self.expression = expression + self.number = '' + self.operator = '' + self.unary_operator = '' + self.rpn = [] + self.stack = [] + self._return_code = 1 + + def _process_digit(self, index, symbol): + """Process digit from expression.""" + if self.expression[index - 1] == ' ' and self.number: + raise CalculatorError('invalid syntax', self._return_code) + self.number += symbol + + def _process_number_and_constant(self): + """Process number and constant.""" + if self.unary_operator: + self.unary_operator = self._replace_unary_operator(self.unary_operator) + + if self.number: + self.rpn.append(self._convert_to_number('{}{}'.format(self.unary_operator, + self.number))) + self.number = '' + + if self.operator in CONSTANTS: + + if self.rpn and self.rpn[-1] in CONSTANTS.values() and not self.stack: + self.stack.append('*') + + if self.unary_operator == '-': + self.rpn.append(0 - CONSTANTS[self.operator]) + else: + self.rpn.append(CONSTANTS[self.operator]) + self.operator = '' + + self.unary_operator = '' + + def _process_operator(self, closing_bracket_index): + """Process operator.""" + if self.unary_operator: + self.stack.append(self.unary_operator) + + if self.operator: + if self.operator not in OPERATORS: + raise CalculatorError('operator not supported', self._return_code) + + self._process_implicit_multiplication(closing_bracket_index - len(self.operator)) + + self.stack.append(self.operator) + + self.unary_operator = '' + self.operator = '' + + def _process_stack(self, symbol): + """Process stack.""" + while self.stack: + if self.stack[-1] == symbol == '^': + break + + if OPERATORS[symbol].priority <= OPERATORS[self.stack[-1]].priority: + self.rpn.append(self.stack.pop()) + else: + break + + self.stack.append(symbol) + + def _process_comparison(self, index, symbol): + """Process comparison.""" + self._process_number_and_constant() + + if self.stack and self.stack[-1] in COMPARISON_SYMBOLS: + if self.expression[index - 1] == ' ': + raise CalculatorError('unexpected whitespace', self._return_code) + self.stack[-1] += symbol + else: + while self.stack: + self.rpn.append(self.stack.pop()) + + self.stack.append(symbol) + + def _process_brackets_and_comma(self, index, symbol): + """Process brackets and comma from expression.""" + if symbol == ',': + self._process_number_and_constant() + while self.stack: + if OPERATORS[symbol].priority < OPERATORS[self.stack[-1]].priority: + self.rpn.append(self.stack.pop()) + else: + break + self.stack.append(symbol) + elif symbol == '(': + if self.number: + self._process_number_and_constant() + self.stack.append('*') + self._process_operator(index) + + self._process_implicit_multiplication(index) + + self.stack.append(symbol) + elif symbol == ')': + self._process_number_and_constant() + while self.stack: + element = self.stack.pop() + if element == '(': + break + self.rpn.append(element) + + if self.stack and OPERATORS[self.stack[-1]].have_brackets: + self.rpn.append(self.stack.pop()) + + def _is_unary_operator(self, index, symbol): + """Define that operator is unary.""" + if symbol not in UNARY_OPERATORS: + return False + if index == 0: + return True + if index <= len(self.expression): + prev_symbol = self._get_previous_symbol(index) + if (prev_symbol in OPERATORS and prev_symbol != ')' + or prev_symbol in COMPARISON_SYMBOLS): + return True + return False + + def _is_floordiv(self, index, symbol): + """Define that operator is flordiv.""" + if index <= len(self.expression): + return symbol == self.expression[index - 1] == '/' + return False + + def _prepare_rpn(self): + """Process expression to reverse polish notation.""" + for index, symbol in enumerate(self.expression): + + if self.operator in CONSTANTS: + self._process_number_and_constant() + + if symbol in COMPARISON_SYMBOLS: + self._process_comparison(index, symbol) + continue + + if symbol.isdigit() and self.operator: + self.operator += symbol + elif symbol.isdigit() or symbol == '.': + self._process_digit(index, symbol) + elif symbol in ('(', ',', ')'): + self._process_brackets_and_comma(index, symbol) + elif symbol in OPERATORS: + if self.stack and self._is_floordiv(index, symbol): + self.stack[-1] += symbol + continue + + if self._is_unary_operator(index, symbol): + self.unary_operator = UNARY_OPERATORS[symbol] + continue + + self._process_number_and_constant() + self._process_stack(symbol) + elif symbol.isalpha() or symbol == '=': + self.operator += symbol + + if symbol != ')': + self._process_implicit_multiplication(index) + + self._process_number_and_constant() + self.rpn.extend(reversed(self.stack)) + + if not self.rpn: + raise CalculatorError('not enough data to calculate', self._return_code) + + del self.stack[:] + + def _process_implicit_multiplication(self, index): + """Сheck for implicit multiplication.""" + prev_symbol = self._get_previous_symbol(index) + if prev_symbol == ')': + self.stack.append('*') + + def _calculate_operator(self, operator): + """Prepare operator to calculate.""" + operator_params = OPERATORS[operator] + + real_params_count = operator_params.params_quantity + if real_params_count == 3: + if self.stack and self.stack[-1] == ',': + self.stack.pop() + real_params_count = 2 + else: + real_params_count = 1 + + if len(self.stack) < real_params_count: + raise CalculatorError("not enough operand's for function {}".format(operator), self._return_code) + elif self.stack and not isinstance(self.stack[-1], (int, float)): + raise CalculatorError("incorrect operand's for function {}".format(operator), self._return_code) + + if real_params_count == 1: + operand = self.stack.pop() + self._calculate_result(operator_params.function, operand) + elif real_params_count == 2: + second_operand = self.stack.pop() + first_operand = self.stack.pop() + self._calculate_result(operator_params.function, first_operand, second_operand) + + def _calculate_result(self, function, first_operand, second_operand=None): + """Calculate function(operator).""" + try: + if second_operand is None: + result = function(first_operand) + else: + result = function(first_operand, second_operand) + except (ZeroDivisionError, ArithmeticError, Exception) as e: + raise CalculatorError(e, self._return_code) + else: + self.stack.append(result) + + def _calculate_rpn(self): + """Calculate reverse polish notation.""" + for item in self.rpn: + if item == ',': + self.stack.append(item) + elif item in UNARY_OPERATORS.values(): + unary_operator = self._replace_unary_operator(item) + self.stack.append(self._convert_to_number('{}1'.format(unary_operator))) + self._calculate_operator('*') + elif item in OPERATORS: + self._calculate_operator(item) + else: + self.stack.append(item) + + def _replace_unary_operator(self, unary_operator): + """Replace unary operator from raw expression.""" + for key, value in UNARY_OPERATORS.items(): + if value == unary_operator: + return key + + def _convert_to_number(self, number): + """Convert number characters to number.""" + return float(number) if '.' in number else int(number) + + def _get_previous_symbol(self, index): + """Return previous symbol excluding whitespace's.""" + for prev_symbol in reversed(self.expression[:index]): + if prev_symbol == ' ': + continue + return prev_symbol + + def calculate(self): + """Prepare and calculate expression.""" + preprocessor = Preprocessor(self.expression) + self.expression = preprocessor.prepare_expression() + self._prepare_rpn() + self._calculate_rpn() + + return self.stack[-1] diff --git a/final_task/pycalc_src/exceptions.py b/final_task/pycalc_src/exceptions.py new file mode 100644 index 0000000..25ff78f --- /dev/null +++ b/final_task/pycalc_src/exceptions.py @@ -0,0 +1,31 @@ +"""Exceptions module.""" + + +class BaseCalculatorException(Exception): + """Base calculator exception.""" + + def __init__(self, message=None, return_code=1): + """"Init.""" + if message is None: + message = 'an error occured while working pycalc' + self.message = 'ERROR: {}'.format(message) + + if return_code == 1: + print(self.message) + exit(return_code) + + +class CalculatorError(BaseCalculatorException): + """Exception for calculator.""" + + def __init__(self, message=None, return_code=1): + """"Init.""" + super().__init__(message, return_code) + + +class PreprocessingError(BaseCalculatorException): + """Exception for preprocessing.""" + + def __init__(self, message=None, return_code=1): + """"Init.""" + super().__init__(message, return_code) diff --git a/final_task/pycalc_src/operators.py b/final_task/pycalc_src/operators.py new file mode 100644 index 0000000..ffc66d6 --- /dev/null +++ b/final_task/pycalc_src/operators.py @@ -0,0 +1,66 @@ +"""Operators, constants, unary operators and comparison symbol's for pycalc.""" + +import operator +import builtins +import math + +from collections import namedtuple + + +UNARY_OPERATORS = {'-': '-@', '+': '+@'} + + +COMPARISON_SYMBOLS = ('!', '<', '>', '=') + + +OPERATOR = namedtuple('OPERATOR', 'priority function params_quantity have_brackets') + + +OPERATORS = { + '+': OPERATOR(1, operator.add, 2, False), + '-': OPERATOR(1, operator.sub, 2, False), + '*': OPERATOR(2, operator.mul, 2, False), + '/': OPERATOR(2, operator.truediv, 2, False), + '//': OPERATOR(2, operator.floordiv, 2, False), + '%': OPERATOR(2, operator.mod, 2, False), + '^': OPERATOR(3, operator.pow, 2, False), + 'pow': OPERATOR(3, operator.pow, 3, True), + + 'sin': OPERATOR(4, math.sin, 1, True), + 'cos': OPERATOR(4, math.cos, 1, True), + 'asin': OPERATOR(4, math.asin, 1, True), + 'acos': OPERATOR(4, math.acos, 1, True), + 'sinh': OPERATOR(4, math.sinh, 1, True), + 'cosh': OPERATOR(4, math.cosh, 1, True), + 'asinh': OPERATOR(4, math.asinh, 1, True), + 'acosh': OPERATOR(4, math.acosh, 1, True), + 'tanh': OPERATOR(4, math.tanh, 1, True), + 'atanh': OPERATOR(4, math.atanh, 1, True), + 'tan': OPERATOR(4, math.tan, 1, True), + 'atan': OPERATOR(4, math.atan, 1, True), + 'hypot': OPERATOR(4, math.hypot, 3, True), + 'atan2': OPERATOR(4, math.atan2, 3, True), + 'exp': OPERATOR(4, math.exp, 1, True), + 'expm1': OPERATOR(4, math.expm1, 1, True), + 'log10': OPERATOR(4, math.log10, 1, True), + 'log2': OPERATOR(4, math.log2, 1, True), + 'log1p': OPERATOR(4, math.log1p, 1, True), + 'sqrt': OPERATOR(4, math.sqrt, 1, True), + 'abs': OPERATOR(4, builtins.abs, 1, True), + 'round': OPERATOR(4, builtins.round, 3, True), + 'log': OPERATOR(4, math.log, 3, True), + + '<': OPERATOR(0, operator.lt, 2, False), + '<=': OPERATOR(0, operator.le, 2, False), + '==': OPERATOR(0, operator.eq, 2, False), + '!=': OPERATOR(0, operator.ne, 2, False), + '>=': OPERATOR(0, operator.ge, 2, False), + '>': OPERATOR(0, operator.gt, 2, False), + ',': OPERATOR(0, None, 0, False), + '(': OPERATOR(0, None, 0, False), + ')': OPERATOR(5, None, 0, False), + '-@': OPERATOR(2, None, 0, False), + '+@': OPERATOR(2, None, 0, False) +} + +CONSTANTS = {a: getattr(math, a) for a in dir(math) if isinstance(getattr(math, a), float)} diff --git a/final_task/pycalc_src/preprocessor.py b/final_task/pycalc_src/preprocessor.py new file mode 100644 index 0000000..049bfbf --- /dev/null +++ b/final_task/pycalc_src/preprocessor.py @@ -0,0 +1,44 @@ +"""Preprocessing module.""" + +from pycalc_src.exceptions import PreprocessingError + +from pycalc_src.operators import OPERATORS +from pycalc_src.operators import CONSTANTS + + +class Preprocessor: + """Preprocessor object.""" + + def __init__(self, expression): + self.expression = expression + self._return_code = 1 + + def prepare_expression(self): + """Prepare expression for calculate.""" + if not self.expression: + raise PreprocessingError('expression is empty', self._return_code) + + if not isinstance(self.expression, str): + raise PreprocessingError('expression is not a string', self._return_code) + + if self.expression.count('(') != self.expression.count(')'): + raise PreprocessingError('brackets are not balanced', self._return_code) + + self.expression = self.expression.lower() + + self.expression = self.expression.replace('**', '^') + + self._clean_repeatable_operators() + + return self.expression + + def _clean_repeatable_operators(self): + """Delete from string repeatable operators.""" + repeatable_operators = {'+-': '-', '--': '+', '++': '+', '-+': '-'} + + while True: + old_exp = self.expression + for old, new in repeatable_operators.items(): + self.expression = self.expression.replace(old, new) + if old_exp == self.expression: + break diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..3aacba0 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup + +setup( + name="pycalc", + version="1.0", + packages=['pycalc_src'], + scripts=['pycalc'], + author="roman.yastremski", + author_email="roman.yastremski@gmail.com", + description="Pure-python command-line calculator." +) diff --git a/final_task/tests/__init__.py b/final_task/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/final_task/tests/test_calculator.py b/final_task/tests/test_calculator.py new file mode 100644 index 0000000..16b7259 --- /dev/null +++ b/final_task/tests/test_calculator.py @@ -0,0 +1,385 @@ +"""Unittest for class Calculator.""" + +import unittest + +import operator +import builtins +import math + +from collections import namedtuple + +from pycalc_src.calculator import Calculator +from pycalc_src.exceptions import BaseCalculatorException + +RETURN_CODE = 0 + + +class TestStringMethods(unittest.TestCase): + """Docstring.""" + + def test_process_digit__valid_expressions(self): + """Docstring.""" + valid_expression = namedtuple('valid_expression', 'expression index symbol result') + valid_expressions = [valid_expression('5', 0, '5', '5'), + valid_expression(' .', 1, '.', '.'), + valid_expression('1 ', 0, '1', '1') + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + calc._process_digit(expression.index, expression.symbol) + + self.assertEqual(calc.number, expression.result) + + def test_process_digit__invalid_expressions(self): + """Docstring.""" + + expression = '1 2 3 4' + + calc = Calculator(expression) + calc._return_code = RETURN_CODE + calc.number = '1' + + with self.assertRaises(BaseCalculatorException): + calc._process_digit(2, '2') + + def test_process_number_and_constant__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'unary_operator number operator result') + valid_expressions = [valid_expression('', '54.55', '', 54.55), + valid_expression('-@', '5', '', -5), + valid_expression('', '', 'pi', math.pi), + valid_expression('-@', '', 'e', -math.e) + ] + + for expression in valid_expressions: + calc = Calculator('') + calc.unary_operator = expression.unary_operator + calc.number = expression.number + calc.operator = expression.operator + calc._process_number_and_constant() + + self.assertEqual(calc.rpn[-1], expression.result) + + def test_process_operator__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'unary_operator operator closing_bracket_index result') + valid_expressions = [valid_expression('', 'sin', 2, ['sin']), + valid_expression('-@', 'log', 2, ['-@', 'log']) + ] + + for expression in valid_expressions: + calc = Calculator('') + calc.unary_operator = expression.unary_operator + calc.operator = expression.operator + calc._process_operator(expression.closing_bracket_index) + + self.assertEqual(calc.stack, expression.result) + + def test_process_operator__invalid_expressions(self): + """Docstring.""" + + invalid_expression = namedtuple('valid_expression', 'unary_operator operator closing_bracket_index') + invalid_expressions = [invalid_expression('', 'log100', 0), + invalid_expression('-@', 'sin4', 0) + ] + + for expression in invalid_expressions: + calc = Calculator('') + calc._return_code = RETURN_CODE + calc.unary_operator = expression.unary_operator + calc.operator = expression.operator + + with self.assertRaises(BaseCalculatorException): + calc._process_operator(expression.closing_bracket_index) + + def test_process_stack__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'stack symbol result_stack result_rpn') + valid_expressions = [valid_expression(['^'], '^', ['^', '^'], []), + valid_expression(['*'], '+', ['+'], ['*']), + valid_expression(['-'], '/', ['-', '/'], []), + valid_expression(['sin', 'tan'], '/', ['/'], ['tan', 'sin']) + ] + + for expression in valid_expressions: + calc = Calculator('') + calc.stack = expression.stack + calc._process_stack(expression.symbol) + + self.assertEqual(calc.stack, expression.result_stack) + self.assertEqual(calc.rpn, expression.result_rpn) + + def test_process_comparison__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression stack index symbol result_stack result_rpn') + valid_expressions = [valid_expression('5 >= 4', ['>'], 3, '=', ['>='], []), + valid_expression('5+1*2 > 4', ['+', '*'], 7, '>', ['>'], ['*', '+']) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + calc.stack = expression.stack + calc._process_comparison(expression.index, expression.symbol) + + self.assertEqual(calc.stack, expression.result_stack) + self.assertEqual(calc.rpn, expression.result_rpn) + + def test_process_comparison__invalid_expressions(self): + """Docstring.""" + + invalid_expression = namedtuple('invalid_expression', 'expression stack index symbol') + invalid_expressions = [invalid_expression('5 > = 4', ['>'], 4, '='), + invalid_expression('5+2 = = 4', ['='], 6, '=') + ] + + for expression in invalid_expressions: + calc = Calculator(expression.expression) + calc._return_code = RETURN_CODE + calc.stack = expression.stack + + with self.assertRaises(BaseCalculatorException): + calc._process_comparison(expression.index, expression.symbol) + + def test_process_brackets_and_comma__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'stack index symbol number result_stack result_rpn') + valid_expressions = [valid_expression(['round', '('], 0, ',', '', ['round', '(', ','], []), + valid_expression(['round', '(', '+'], 0, ',', '', ['round', '(', ','], ['+']), + valid_expression(['+'], 0, '(', '', ['+', '('], []), + valid_expression(['+'], 0, '(', '2', ['+', '*', '('], [2]), + valid_expression(['(', '+', '*'], 0, ')', '', [], ['*', '+']), + valid_expression(['+', '(', '*'], 0, ')', '', ['+'], ['*']) + ] + + for expression in valid_expressions: + calc = Calculator('expression') + calc.stack = expression.stack + calc.number = expression.number + calc._process_brackets_and_comma(expression.index, expression.symbol) + + self.assertEqual(calc.stack, expression.result_stack) + self.assertEqual(calc.rpn, expression.result_rpn) + + def test_is_unary_operator__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression index symbol result') + valid_expressions = [valid_expression('-4', 0, '-', True), + valid_expression('- 4', 0, '-', True), + valid_expression('!4', 0, '!', False), + valid_expression('-4', 4, '-', False), + valid_expression('1*-4', 2, '-', True), + valid_expression('(1*2)-4', 5, '-', False), + valid_expression('5==-5', 3, '-', True) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + func_result = calc._is_unary_operator(expression.index, expression.symbol) + + if expression.result: + self.assertTrue(func_result) + else: + self.assertFalse(func_result) + + def test_is_floordiv__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression index symbol result') + valid_expressions = [valid_expression('5/5', 4, '', False), + valid_expression('4//3', 2, '/', True), + valid_expression('4/3', 1, '/', False) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + func_result = calc._is_floordiv(expression.index, expression.symbol) + + if expression.result: + self.assertTrue(func_result) + else: + self.assertFalse(func_result) + + def test_prepare_rpn__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression result_rpn') + valid_expressions = [valid_expression('pi', [3.141592653589793]), + valid_expression('<=', ['<=']), + valid_expression('log2()', ['log2']), + valid_expression('51.567', [51.567]), + valid_expression('round(1.233333, 2)', [1.233333, 2, ',', 'round']), + valid_expression('81//8', [81, 8, '//']), + valid_expression('//', ['//']), + valid_expression('-100', [-100]), + valid_expression('pi*log2(1)==-1', [3.141592653589793, 1, 'log2', '*', -1, '==']) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + calc._prepare_rpn() + + self.assertEqual(calc.rpn, expression.result_rpn) + + def test_prepare_rpn__invalid_expressions(self): + """Docstring.""" + + invalid_expression = namedtuple('invalid_expression', 'expression') + invalid_expressions = [invalid_expression('not an expression') + ] + + for expression in invalid_expressions: + calc = Calculator(expression.expression) + calc._return_code = RETURN_CODE + + with self.assertRaises(BaseCalculatorException): + calc._prepare_rpn() + + def test_calculate_operator__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression stack operator result_stack') + valid_expressions = [valid_expression('1+2', [1, 2], '+', [3]), + valid_expression('round(1.2254,2)', [1.2254, 2, ','], 'round', [1.23]), + valid_expression('log(.5)', [0.5], 'log', [-0.6931471805599453]) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + calc.stack = expression.stack + calc._calculate_operator(expression.operator) + + self.assertEqual(calc.stack, expression.result_stack) + + def test_calculate_operator__invalid_expressions(self): + """Docstring.""" + + invalid_expression = namedtuple('invalid_expression', 'expression stack operator') + invalid_expressions = [invalid_expression('log(.5,)', [0.5, ','], 'log'), + invalid_expression('log(.5,1,2)', [0.5, 1, 2, ',', ','], 'log') + ] + + for expression in invalid_expressions: + calc = Calculator(expression.expression) + calc._return_code = RETURN_CODE + calc.stack = expression.stack + + with self.assertRaises(BaseCalculatorException): + calc._calculate_operator(expression.operator) + + def test_calculate_result__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', + 'expression function first_operand second_operand result_stack') + valid_expressions = [valid_expression('365+635', operator.add, 365, 635, [1000]), + valid_expression('sin(1)', math.sin, 1, None, [0.8414709848078965]) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + calc._calculate_result(expression.function, expression.first_operand, expression.second_operand) + + self.assertEqual(calc.stack, expression.result_stack) + + def test_calculate_result__invalid_expressions(self): + """Docstring.""" + + invalid_expression = namedtuple('invalid_expression', 'expression function first_operand second_operand') + invalid_expressions = [invalid_expression('5/0', operator.truediv, 5, 0), + invalid_expression('log(-100)', math.log, -100, None), + invalid_expression('log(1,,)', math.log, 1, ',') + ] + + for expression in invalid_expressions: + calc = Calculator(expression.expression) + calc._return_code = RETURN_CODE + + with self.assertRaises(BaseCalculatorException): + calc._calculate_result(expression.function, expression.first_operand, expression.second_operand) + + def test_calculate_rpn__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression rpn result_stack') + valid_expressions = [valid_expression(',', [','], [',']), + valid_expression('-(3)', [3, '-@'], [-3]), + valid_expression('1+cos(1)', [1, 1, 'cos', '+'], [1.5403023058681398]), + valid_expression('1563', [1563], [1563]) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + calc.rpn = expression.rpn + calc._calculate_rpn() + + self.assertEqual(calc.stack, expression.result_stack) + + def test_replace_unary_operator__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'unary_operator result') + valid_expressions = [valid_expression('-@', '-'), + valid_expression('+@', '+') + ] + + for expression in valid_expressions: + calc = Calculator('expression') + + result = calc._replace_unary_operator(expression.unary_operator) + + self.assertEqual(result, expression.result) + + def test_convert_to_number__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'number result') + valid_expressions = [valid_expression('569', 569), + valid_expression('789.99', 789.99), + valid_expression('-500.87', -500.87) + ] + + for expression in valid_expressions: + calc = Calculator('expression') + + result = calc._convert_to_number(expression.number) + + self.assertEqual(result, expression.result) + + def test_process_implicit_multiplication__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression index result_stack') + valid_expressions = [valid_expression('(1)(1+2)', 3, ['*']) + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + + calc._process_implicit_multiplication(expression.index) + + self.assertEqual(calc.stack, expression.result_stack) + + def test_get_previous_symbol__valid_expressions(self): + """Docstring.""" + + valid_expression = namedtuple('valid_expression', 'expression index result') + valid_expressions = [valid_expression('1 + 2', 5, '+') + ] + + for expression in valid_expressions: + calc = Calculator(expression.expression) + + result = calc._get_previous_symbol(expression.index) + + self.assertEqual(result, expression.result) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/tests/test_exceptions.py b/final_task/tests/test_exceptions.py new file mode 100644 index 0000000..607000f --- /dev/null +++ b/final_task/tests/test_exceptions.py @@ -0,0 +1,20 @@ +"""Unittest for module exeptions.""" + +import unittest + +from pycalc_src.exceptions import BaseCalculatorException +from pycalc_src.exceptions import CalculatorError + + +class TestStringMethods(unittest.TestCase): + """Docstring.""" + + def test_base_calculator_exeption__valid_expressions(self): + """Docstring.""" + + with self.assertRaises(BaseCalculatorException): + raise CalculatorError(None, 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/tests/test_preprocessor.py b/final_task/tests/test_preprocessor.py new file mode 100644 index 0000000..9fff770 --- /dev/null +++ b/final_task/tests/test_preprocessor.py @@ -0,0 +1,63 @@ +"""Unittest for module preprocessing.""" + +import unittest + +from collections import namedtuple + +from pycalc_src.preprocessor import Preprocessor +from pycalc_src.exceptions import BaseCalculatorException + +RETURN_CODE = 0 + + +class TestStringMethods(unittest.TestCase): + """Docstring.""" + + def test_prepare_expression__valid_expressions(self): + """Docstring.""" + valid_expression = namedtuple('valid_expression', 'expression result') + valid_expressions = [valid_expression('TAN(1)', 'tan(1)'), + valid_expression('**', '^') + ] + + for expression in valid_expressions: + preprocessor = Preprocessor(expression.expression) + func_result = preprocessor.prepare_expression() + + self.assertEqual(func_result, expression.result) + + def test_preprocessing__invalid_expressions(self): + """Docstring.""" + + invalid_expression = namedtuple('valid_expression', 'expression') + invalid_expressions = [invalid_expression(''), + invalid_expression(set()), + invalid_expression('(()'), + ] + + for expression in invalid_expressions: + preprocessor = Preprocessor(expression.expression) + preprocessor._return_code = RETURN_CODE + + with self.assertRaises(BaseCalculatorException): + result = preprocessor.prepare_expression() + + def test_clean_repeatable_operators__valid_expressions(self): + """Docstring.""" + valid_expression = namedtuple('valid_expression', 'expression result') + valid_expressions = [valid_expression('--1', '+1'), + valid_expression('-+2', '-2'), + valid_expression('++2.4', '+2.4'), + valid_expression('-+-+-++++------3', '-3'), + valid_expression('-+-3++', '+3+') + ] + + for expression in valid_expressions: + preprocessor = Preprocessor(expression.expression) + preprocessor._clean_repeatable_operators() + + self.assertEqual(preprocessor.expression, expression.result) + + +if __name__ == '__main__': + unittest.main()