From a8007a1f5083d6283923f531a2d1964e9de43022 Mon Sep 17 00:00:00 2001 From: DaniilMaloletni <50489139+DaniilMaloletni@users.noreply.github.com> Date: Fri, 10 May 2019 17:12:23 +0300 Subject: [PATCH 01/11] Release --- final_task/Test.py | 165 +++++++++++++++ final_task/pycalc.py | 488 +++++++++++++++++++++++++++++++++++++++++++ final_task/setup.py | 10 + 3 files changed, 663 insertions(+) create mode 100644 final_task/Test.py create mode 100644 final_task/pycalc.py diff --git a/final_task/Test.py b/final_task/Test.py new file mode 100644 index 00000000..b512c8fc --- /dev/null +++ b/final_task/Test.py @@ -0,0 +1,165 @@ +import unittest +import math +import pycalc + + +class TestMyCaseCalculator(unittest.TestCase): + + def test_default_arithmetic(self): + self.assertEqual(pycalc.py_calculator('10+10'), 10+10) + self.assertEqual(pycalc.py_calculator('2*4'), 2*4) + self.assertEqual(pycalc.py_calculator('5-6'), 5-6) + self.assertEqual(pycalc.py_calculator('10/10'), 10/10) + self.assertEqual(pycalc.py_calculator('10^10'), 10**10) + self.assertEqual(pycalc.py_calculator('21//2'), 21//2) + self.assertEqual(pycalc.py_calculator('2%2'), 2 % 2) + + def test_with_floating_numbers(self): + self.assertEqual(pycalc.py_calculator('0.4 + 1.5'), 0.4 + 1.5) + self.assertEqual(pycalc.py_calculator('.4 + 1.5'), .4 + 1.5) + self.assertEqual(pycalc.py_calculator('.4^-1.5'), eval('0.4**-1.5')) + + def test_number_theoretic_and_representation_functions(self): + self.assertEqual(pycalc.py_calculator('ceil(2.5)'), math.ceil(2.5)) + self.assertEqual(pycalc.py_calculator('copysign(1.0, -1.0)'), math.copysign(1.0, -1.0)) + self.assertEqual(pycalc.py_calculator('fabs(-5)'), math.fabs(-5)) + self.assertEqual(pycalc.py_calculator('factorial(2)'), math.factorial(2)) + self.assertEqual(pycalc.py_calculator('floor(5.5)'), math.floor(5.5)) + self.assertEqual(pycalc.py_calculator('fmod(5,5)'), math.fmod(5, 5)) + self.assertEqual(pycalc.py_calculator('frexp(5)'), math.frexp(5)) + self.assertEqual(pycalc.py_calculator('ldexp(3,10)'), math.ldexp(3, 10)) + self.assertEqual(pycalc.py_calculator('fsum([.1, .1, .1])'), math.fsum([.1, .1, .1])) + self.assertEqual(pycalc.py_calculator('gcd(5, 10)'), math.gcd(5, 10)) + self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05, abs_tol=0.0)'), + math.isclose(1, 2, rel_tol=0.05, abs_tol=0.0)) + self.assertEqual(pycalc.py_calculator('isfinite(3)'), math.isfinite(3)) + self.assertEqual(pycalc.py_calculator('isinf(3)'), math.isinf(3)) + self.assertEqual(pycalc.py_calculator('isnan(3)'), math.isnan(3)) + self.assertEqual(pycalc.py_calculator('modf(-3)'), math.modf(-3)) + self.assertEqual(pycalc.py_calculator('trunc(3.4)'), math.trunc(3.4)) + self.assertEqual(pycalc.py_calculator('exp(3)'), math.exp(3)) + self.assertEqual(pycalc.py_calculator('expm1(3)'), math.expm1(3)) + self.assertEqual(pycalc.py_calculator('log(10,2)'), math.log(10, 2)) + self.assertEqual(pycalc.py_calculator('log1p(10)'), math.log1p(10)) + self.assertEqual(pycalc.py_calculator('log10(10)'), math.log10(10)) + self.assertEqual(pycalc.py_calculator('log2(10)'), math.log2(10)) + self.assertEqual(pycalc.py_calculator('pow(2,3)'), math.pow(2, 3)) + self.assertEqual(pycalc.py_calculator('sqrt(25)'), math.sqrt(25)) + self.assertEqual(pycalc.py_calculator('erf(3)'), math.erf(3)) + self.assertEqual(pycalc.py_calculator('erfc(3)'), math.erfc(3)) + self.assertEqual(pycalc.py_calculator('gamma(3)'), math.gamma(3)) + self.assertEqual(pycalc.py_calculator('lgamma(3)'), math.lgamma(3)) + + def test_module_math_trigonometry(self): + self.assertEqual(pycalc.py_calculator('sin(90)'), math.sin(90)) + self.assertEqual(pycalc.py_calculator('cos(90)'), math.cos(90)) + self.assertEqual(pycalc.py_calculator('tan(90)'), math.tan(90)) + self.assertEqual(pycalc.py_calculator('asin(1)'), math.asin(1)) + self.assertEqual(pycalc.py_calculator('acos(0)'), math.acos(0)) + self.assertEqual(pycalc.py_calculator('atan(1)'), math.atan(1)) + self.assertEqual(pycalc.py_calculator('hypot(3,4)'), math.hypot(3, 4)) + self.assertEqual(pycalc.py_calculator('degrees(3.14)'), math.degrees(3.14)) + self.assertEqual(pycalc.py_calculator('radians(90)'), math.radians(90)) + self.assertEqual(pycalc.py_calculator('sinh(1)'), math.sinh(1)) + self.assertEqual(pycalc.py_calculator('cosh(1)'), math.cosh(1)) + self.assertEqual(pycalc.py_calculator('tanh(1)'), math.tanh(1)) + self.assertEqual(pycalc.py_calculator('asinh(1)'), math.asinh(1)) + self.assertEqual(pycalc.py_calculator('acosh(1)'), math.acosh(1)) + self.assertEqual(pycalc.py_calculator('atanh(0)'), math.atanh(0)) + self.assertEqual(pycalc.py_calculator('pi'), math.pi) + self.assertEqual(pycalc.py_calculator('e'), math.e) + self.assertEqual(pycalc.py_calculator('tau'), math.tau) + + def test_round_brackets(self): + self.assertEqual(pycalc.py_calculator('(2+2)*2'), (2+2)*2) + self.assertEqual(pycalc.py_calculator('(2+2)*2+(2+2)'), (2+2)*2+(2+2)) + self.assertEqual(pycalc.py_calculator('2+(2+(2+3)+3)+2'), 2+(2+(2+3)+3)+2) + self.assertEqual(pycalc.py_calculator('2+(2+3)*3+2'), 2+(2+3)*3+2) + self.assertEqual(pycalc.py_calculator('((2+2)*3)+2'), ((2+2)*3)+2) + + +class TestEpamCaseCalculator(unittest.TestCase): + + def test_unary_operators(self): + self.assertEqual(pycalc.py_calculator("-13"), -13) + self.assertEqual(pycalc.py_calculator("6-(-13)"), 6-(-13)) + self.assertEqual(pycalc.py_calculator("1---1"), 1---1) + self.assertEqual(pycalc.py_calculator("-+---+-1"), -+---+-1) + + def test_operation_priority(self): + self.assertEqual(pycalc.py_calculator("1+2*2"), 1+2*2) + self.assertEqual(pycalc.py_calculator("1+(2+3*2)*3"), 1+(2+3*2)*3) + self.assertEqual(pycalc.py_calculator("10*(2+1)"), 10*(2+1)) + self.assertEqual(pycalc.py_calculator("10^(2+1)"), 10**(2+1)) + self.assertEqual(pycalc.py_calculator("100/3^2"), 100/3**2) + self.assertEqual(pycalc.py_calculator("100/3%2^2"), 100/3 % 2**2) + + def test_functions_and_constants(self): + self.assertEqual(pycalc.py_calculator("pi+e"), math.pi+math.e) + self.assertEqual(pycalc.py_calculator("log(e)"), math.log(math.e)) + self.assertEqual(pycalc.py_calculator("sin(pi/2)"), math.sin(math.pi/2)) + self.assertEqual(pycalc.py_calculator("log10(100)"), math.log10(100)) + self.assertEqual(pycalc.py_calculator("sin(pi/2)*111*6"), math.sin(math.pi/2)*111*6) + self.assertEqual(pycalc.py_calculator("2*sin(pi/2)"), 2*math.sin(math.pi/2)) + self.assertEqual(pycalc.py_calculator("pow(2, 3)"), math.pow(2, 3)) + self.assertEqual(pycalc.py_calculator("abs(-5)"), abs(-5)) + self.assertEqual(pycalc.py_calculator("round(123.4567890)"), round(123.4567890)) + + def test_associative(self): + self.assertEqual(pycalc.py_calculator("102%12%7"), 102 % 12 % 7) + self.assertEqual(pycalc.py_calculator("100/4/3"), 100/4/3) + self.assertEqual(pycalc.py_calculator("2^3^4"), 2**3**4) + + def test_comparison_operators(self): + self.assertEqual(pycalc.py_calculator("1+2*3==1+2*3"), 1+2*3 == 1+2*3) + self.assertAlmostEqual(pycalc.py_calculator("e^5>=e^5+1"), math.e**5 >= math.e**5+1) + self.assertAlmostEqual(pycalc.py_calculator("1+2*4/3+1!=1+2*4/3+2"), 1+2*4/3+1 != 1+2*4/3+2) + + def test_common_tests(self): + self.assertEqual(pycalc.py_calculator("(100)"), eval("(100)")) + self.assertEqual(pycalc.py_calculator("666"), 666) + self.assertEqual(pycalc.py_calculator("-.1"), -.1) + self.assertEqual(pycalc.py_calculator("1/3"), 1/3) + self.assertEqual(pycalc.py_calculator("1.0/3.0"), 1.0/3.0) + self.assertEqual(pycalc.py_calculator(".1 * 2.0^56.0"), .1 * 2.0**56.0) + self.assertEqual(pycalc.py_calculator("e^34"), math.e**34) + self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))"), + (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))) + self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)"), + (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))**(1.0/3.0)) + self.assertEqual(pycalc.py_calculator("sin(pi/2^1) + log(1*4+2^2+1, 3^2)"), + math.sin(math.pi/2**1) + math.log(1*4+2**2+1, 3**2)) + self.assertEqual(pycalc.py_calculator("10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"), + 10*math.e**0*math.log10(.4 - 5 / -0.1-10) - -abs(-53/10) + -5) + expression = "sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+" \ + "cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)" + self.assertEqual(pycalc.py_calculator(expression), + math.sin(-math.cos(-math.sin(3.0)-math.cos(-math.sin(-3.0*5.0)-math.sin(math.cos(math.log10(43.0)))) + + math.cos(math.sin(math.sin(34.0-2.0**2.0))))--math.cos(1.0)--math.cos(0.0)**3.0)) + self.assertEqual(pycalc.py_calculator("2.0^(2.0^2.0*2.0^2.0)"), 2.0**(2.0**2.0*2.0**2.0)) + self.assertEqual(pycalc.py_calculator("sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))"), + math.sin(math.e**math.log(math.e**math.e**math.sin(23.0), 45.0) + + math.cos(3.0+math.log10(math.e**-math.e)))) + + def test_Error_cases(self): + self.assertRaises(ValueError, pycalc.py_calculator, "") + self.assertRaises(ValueError, pycalc.py_calculator, "+") + self.assertRaises(ValueError, pycalc.py_calculator, "1-") + self.assertRaises(ValueError, pycalc.py_calculator, "1 2") + self.assertRaises(ValueError, pycalc.py_calculator, "==7") + self.assertRaises(ValueError, pycalc.py_calculator, "1 + 2(3 * 4))") + self.assertRaises(ValueError, pycalc.py_calculator, "((1+2)") + self.assertRaises(ValueError, pycalc.py_calculator, "1 + 1 2 3 4 5 6 ") + self.assertRaises(ValueError, pycalc.py_calculator, "log100(100)") + self.assertRaises(ValueError, pycalc.py_calculator, "------") + self.assertRaises(ValueError, pycalc.py_calculator, "5 > = 6") + self.assertRaises(ValueError, pycalc.py_calculator, "5 / / 6") + self.assertRaises(ValueError, pycalc.py_calculator, "6 < = 6") + self.assertRaises(ValueError, pycalc.py_calculator, "6 * * 6") + self.assertRaises(ValueError, pycalc.py_calculator, "(((((") + self.assertRaises(ValueError, pycalc.py_calculator, "abs") + self.assertRaises(ValueError, pycalc.py_calculator, "pow(2, 3, 4)") + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/pycalc.py b/final_task/pycalc.py new file mode 100644 index 00000000..3ec1e15f --- /dev/null +++ b/final_task/pycalc.py @@ -0,0 +1,488 @@ +""" + +The module calculates mathematical expressions. + +Functions: + py_calculator: consists of 4 stages(parse, check expression, transform to polish notation, calculate); + parse: split an expression into numbers, functions, constants, operations; + check_expression: validates the expression entered; + transform_to_polish_notation: converts expressions according to the rules of reverse polish notation; + calculate: calculates expressions written in reverse polish notation. + +Attributes: + OPERATORS : dictionary of operations and their priority; + BUILT_IN_FUNCTION: dictionary of numeric functions; + FUNCTIONS: dictionary of mathematical functions; + CONSTANTS: dictionary of mathematical constants; + LOGIC_OPERATORS: list of logical operator character; + NEGATIVE_FLOAT_TYPE1: possible representations of a negative float number like -.5; + NEGATIVE_FLOAT_TYPE2: possible representations of a negative float number like -0.5; + NEGATIVE_INTEGER: possible representations of a negative integer number like -5; + FLOAT_TYPE1: possible representations of a float number like 0.5; + FLOAT_TYPE2: possible representations of a float number like .5; + INTEGER: possible representations of a integer number like 5; + +""" + +import argparse +import math +import operator +import string +import re +from json import loads + +OPERATORS = { + '+': (operator.add, 2), + '-': (operator.sub, 2), + '*': (operator.mul, 3), + '/': (operator.truediv, 3), + '//': (operator.floordiv, 3), + '%': (operator.mod, 3), + '^': (operator.pow, 4), + '=': (operator.eq, 0), + '==': (operator.eq, 0), + '<': (operator.lt, 0), + '<=': (operator.le, 0), + '!=': (operator.ne, 0), + '>=': (operator.ge, 0), + '>': (operator.gt, 0), +} + +BUILT_IN_FUNCTION = { + 'abs': abs, + 'round': round +} + +FUNCTIONS = {attr: getattr(math, attr) for attr in dir(math) if callable(getattr(math, attr))} + +CONSTANTS = { + 'e': math.e, + 'pi': math.pi, + 'tau': math.tau, + '-e': -math.e, + '-pi': -math.pi, + '-tau': -math.tau, + 'inf': math.inf, + '-inf': -math.inf, + 'nan': math.inf +} + +LOGIC_OPERATORS = ['=', '!', '<', '>'] + +NEGATIVE_FLOAT_TYPE1 = re.compile(r'^-\.\d+$') +NEGATIVE_FLOAT_TYPE2 = re.compile(r'^-\d+\.\d+$') +NEGATIVE_INTEGER = re.compile(r'^-\d+$') +FLOAT_TYPE1 = re.compile(r'^\d+\.\d+$') +FLOAT_TYPE2 = re.compile(r'^\.\d+$') +INTEGER = re.compile(r'^\d+$') + + +def py_calculator(math_expression): + """ + + :param math_expression: String of mathematic expression + :return: Result of calculation + + """ + def parse(expression: str) -> list: + """Split mathematic expression, consists of four steps + + :param expression: String of mathematic expression + :return: Parsed line by items in the list + + """ + def parse_step1(expr: str) -> list: + """Сreates a list with numbers, functions and constants from a mathematical expression + + :param expr: String of math expression + :return: List with items of expression + :raises: ValueError if nothing entered or invalid expression + + """ + parse_list = [] + number = '' + function = '' + if bool(re.compile(r'[\d\w()]\s+[.\d\w()]').findall(expr)) \ + or bool(re.compile(r'[+-/*%^=<>]$').findall(expr)) \ + or bool(re.compile(r'^[/*%^=<>!]').findall(expr)) \ + or bool(re.compile(r'[+-/*%^=<>]\)').findall(expr)) \ + or bool(re.compile(r'[<>/*=!]\s+[=/*]').findall(expr)) \ + or bool(re.compile(r'\(\)').findall(expr)): + raise ValueError(f'invalid expression') + elif not expr: + raise ValueError(f'nothing entered') + for element in expr: + if element in string.ascii_letters or element == '_': + function += element + elif element in string.digits or element == '.': + number += element + parse_list.append(function) + function = '' + elif element in OPERATORS or element in LOGIC_OPERATORS or element in "(,)": + parse_list.append(function) + parse_list.append(number) + parse_list.append(element) + number = '' + function = '' + if number: + parse_list.append(number) + elif function: + parse_list.append(function) + return list(filter(None, parse_list)) + + def parse_step2(parse_list: list) -> list: + """Finishes the parsing functions and collects logical operators and //. + + :param parse_list: List with items of expression + :return: Updated list. + :raise: ValueError if function arguments not entered + + """ + try: + for index, element in enumerate(parse_list): + if element == 'log' and parse_list[index + 1] == 'p' and parse_list[index + 2] == '1': + parse_list[index] += parse_list.pop(index + 2) + parse_list.pop(index + 1) + elif (element in FUNCTIONS or element in BUILT_IN_FUNCTION) and parse_list[index + 1] != '(': + parse_list[index] += parse_list.pop(index + 1) + elif element == 'expm' and parse_list[index + 1] == '1': + parse_list[index] += parse_list.pop(index + 1) + elif element == '/' and parse_list[index + 1] == '/': + parse_list[index] += parse_list.pop(index + 1) + elif element in LOGIC_OPERATORS and parse_list[index + 1] == '=': + parse_list[index] += parse_list.pop(index + 1) + except IndexError: + raise ValueError(f'function arguments not entered') + else: + return parse_list + + def parse_step3(parse_list: list) -> list: + """Converts multiple + and -. + + :param parse_list: List with items of expression. + :return: Updated list. + + """ + for elem in range(len(parse_list)): + for index, element in enumerate(parse_list): + if element == '-' and parse_list[index + 1] == '-' \ + or element == '+' and parse_list[index + 1] == '+': + parse_list[index] = '+' + parse_list.pop(index + 1) + elif element == '+' and parse_list[index + 1] == '-' \ + or element == '-' and parse_list[index + 1] == '+': + parse_list[index] = '-' + parse_list.pop(index + 1) + return parse_list + + def parse_step4(parse_list: list) -> list: + """Working with minuses in expression. + + :param parse_list: List with items of expression. + :return: Updated list. + + """ + try: + for index, element in enumerate(parse_list): + if element == '-' and (parse_list[index + 1].isdigit() + or parse_list[index + 1].replace('.', '', 1).isdigit() + or parse_list[index + 1] in CONSTANTS): + if parse_list[index - 1] in '(*/%//^,': + parse_list[index] += parse_list.pop(index + 1) + elif parse_list.index(element) == 0: + parse_list[index] += parse_list.pop(index + 1) + elif parse_list[index + 2] in '*/%//': + parse_list[index] += parse_list.pop(index + 1) + parse_list.insert(index, '+') + elif element == '-' and parse_list[index + 1] in FUNCTIONS and parse_list[index - 1] == '(': + parse_list[index] = '-1' + parse_list.insert(index + 1, '*') + except IndexError: + return parse_list + else: + return parse_list + return parse_step4(parse_step3(parse_step2(parse_step1(expression)))) + + def check_expression(parse_expression): + """Contains functions that validate the input expression. + + :param parse_expression: List with items of expression + :return: List with expression elements if the expression passed all checks + + """ + def check_operators(parse_list): + """Checks the validity of the entered operators. + + :param parse_list: List with items of expression. + :raise: ValueError if operators follow each other. + + """ + + for index, element in enumerate(parse_list): + if element in OPERATORS and parse_list[index + 1] in OPERATORS and parse_list[index + 1] != '-': + raise ValueError(f'unknown operation {element + parse_list[index + 1]}') + return parse_list + + def check_function_and_constants(parse_list): + """Checks the validity of the entered functions or constants + + :param parse_list: List with items of expression. + :raise: ValueError if functions or constants are not supported by a calculator or are incorrectly entered. + + """ + copy_parse_expression = parse_list.copy() + for index, element in enumerate(copy_parse_expression): + if element == ''.join(INTEGER.findall(element)) \ + or element == ''.join(FLOAT_TYPE1.findall(element)) \ + or element == ''.join(FLOAT_TYPE2.findall(element)) \ + or element == ''.join(NEGATIVE_INTEGER.findall(element)) \ + or element == ''.join(NEGATIVE_FLOAT_TYPE1.findall(element)) \ + or element == ''.join(NEGATIVE_FLOAT_TYPE2.findall(element)): + copy_parse_expression.pop(index) + diff = set(copy_parse_expression).difference( + set(FUNCTIONS), + set(OPERATORS), + set(CONSTANTS), + set(BUILT_IN_FUNCTION), + set(string.digits), + {'{', '[', '(', ',', ')', ']', '}'}, + {'True', 'False', 'abs_tol', 'rel_tol'} + ) + if diff: + raise ValueError(f'unknown function or constant {diff}') + else: + return parse_list + + def check_brackets(parse_list): + """Check count of '(',')'. + + :param parse_list: List with items of expression. + :raise: ValueError if Amount '(' not equal to quantity ')' in expression + + """ + if parse_list.count('(') != parse_list.count(')'): + raise ValueError(f'brackets are not balanced') + else: + return parse_list + + return check_operators(check_brackets(check_function_and_constants(parse_expression))) + + class FuncIsclose: + def __init__(self, arg1, arg2, arg3=1e-09, arg4=0.0): + self.arg1 = arg1 + self.arg2 = arg2 + self.rel_tol = arg3 + self.abs_tol = arg4 + + def calc(self): + return math.isclose(self.arg1, self.arg2, rel_tol=self.rel_tol, abs_tol=self.abs_tol) + + def exec_isclose(parse_list): + """ + + :param parse_list: List with items of expression. + :return: List with isclose function result. + :raise: ValueError if entered invalid number of arguments. + """ + arguments = [] + for element in parse_list: + if element == 'isclose': + for argument in parse_list: + if argument == ''.join(INTEGER.findall(argument)) \ + or argument == ''.join(FLOAT_TYPE1.findall(argument)) \ + or argument == ''.join(FLOAT_TYPE2.findall(argument)) \ + or argument == ''.join(NEGATIVE_INTEGER.findall(argument)) \ + or argument == ''.join(NEGATIVE_FLOAT_TYPE1.findall(argument)) \ + or argument == ''.join(NEGATIVE_FLOAT_TYPE2.findall(argument)): + arguments.append(float(argument)) + if len(arguments) == 1 or len(arguments) > 4: + raise ValueError(f'invalid number of arguments') + elif len(arguments) == 2: + isclose = FuncIsclose(arguments[0], arguments[1]) + return [str(isclose.calc())] + elif len(arguments) == 3: + isclose = FuncIsclose(arguments[0], arguments[1], arguments[2]) + return [str(isclose.calc())] + elif len(arguments) == 4: + isclose = FuncIsclose(arguments[0], arguments[1], arguments[2], arguments[3]) + return [str(isclose.calc())] + else: + return parse_list + + def transform_to_polish_notation(parse_expression: list) -> str: + """ + + :param parse_expression: list after parsing + :return: string in reverse polish notation + + """ + stack = [] + reverse_polish_notation = '' + separator = ' ' + for token in parse_expression: + if token == ''.join(INTEGER.findall(token)) \ + or token == ''.join(FLOAT_TYPE1.findall(token)) \ + or token == ''.join(FLOAT_TYPE2.findall(token)): + reverse_polish_notation += token + separator + elif token == ''.join(NEGATIVE_INTEGER.findall(token)) \ + or token == ''.join(NEGATIVE_FLOAT_TYPE1.findall(token)) \ + or token == ''.join(NEGATIVE_FLOAT_TYPE2.findall(token)): + # writes a negative number of -5 in the form of 0 5 -, according to the rules of the reverse notation + reverse_polish_notation += '0' + separator + token[1:] + separator + token[0] + separator + elif token == ')': + for element in stack[::-1]: + if element == '(': + break + reverse_polish_notation += stack.pop() + separator + stack.pop() + elif token == ',': + for element in stack[::-1]: + if element == '(': + break + reverse_polish_notation += stack.pop() + separator + reverse_polish_notation += token + separator + elif token == '(': + stack.append(token) + elif token in FUNCTIONS or token in BUILT_IN_FUNCTION: + stack.append(token) + elif token in OPERATORS: + if not stack: + stack.append(token) + elif token == stack[-1] and token == '^': + stack.append(token) + elif stack[-1] == '(': + stack.append(token) + elif stack[-1] in FUNCTIONS or stack[-1] in BUILT_IN_FUNCTION: + reverse_polish_notation += stack.pop() + separator + if not stack: + stack.append(token) + elif stack[-1] in OPERATORS: + if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + stack.append(token) + elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: + stack.append(token) + elif token == '-' and parse_expression[parse_expression.index(token) - 1] in '/*^%//': + if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + stack.append(token) + reverse_polish_notation += '0' + separator + elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + if stack: + if stack[-1] == '(': + stack.append(token) + elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + stack.append(token) + elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: + stack.append(token) + elif not stack: + stack.append(token) + else: + stack.append(token) + elif token in CONSTANTS: + reverse_polish_notation += token + separator + elif token in ('True', 'False'): + reverse_polish_notation += token + separator + while stack: + reverse_polish_notation += stack.pop() + separator + return reverse_polish_notation + + def quantity_of_arguments(function): + """From the description of the function determines how many arguments it includes + + :param function: mathematical or numerical functions + :return: quantity of arguments + + """ + spec = function.__doc__.split('\n')[0] + args = spec[spec.find('(') + 1:spec.find(')')] + return args.count(',') + 1 if args else 0 + + def calculate(reverse_polish_notation): + stack = [0] + for token in reverse_polish_notation.split(' '): + if token in OPERATORS: + op2, op1 = stack.pop(), stack.pop() + stack.append(OPERATORS[token][0](op1, op2)) + elif token in BUILT_IN_FUNCTION: + if stack[-2] == ',': + op2 = stack.pop() + stack.pop() + op1 = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + else: + stack.append(BUILT_IN_FUNCTION[token](op1, int(op2))) + else: + op = stack.pop() + stack.append(BUILT_IN_FUNCTION[token](op)) + elif token in FUNCTIONS: + if quantity_of_arguments(FUNCTIONS[token]) == 1: + if token == 'fsum': + numbers = [] + for number in stack[::-1]: + if isinstance(number, (int, float)) and stack[::-1][1] == ',': + numbers.append(stack.pop()) + stack.pop() + elif stack[::-1][0] == ',' and isinstance(number, (int, float)): + raise ValueError(f'invalid expression') + numbers.append(stack.pop()) + stack.append(numbers) + op = stack.pop() + stack.append(FUNCTIONS[token](op)) + numbers.clear() + else: + op = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + stack.append(FUNCTIONS[token](op)) + elif quantity_of_arguments(FUNCTIONS[token]) == 2: + if stack[-2] == ',': + op2 = stack.pop() + stack.pop() + op1 = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + elif token == 'ldexp': + stack.append(FUNCTIONS[token](op1, int(op2))) + elif token == 'gcd': + stack.append(FUNCTIONS[token](int(op1), int(op2))) + else: + stack.append(FUNCTIONS[token](op1, op2)) + else: + op = stack.pop() + stack.append(FUNCTIONS[token](op)) + elif token == ',': + stack.append(token) + elif token in CONSTANTS: + stack.append(CONSTANTS[token]) + elif token in ('True', 'False'): + stack.append(loads(token.lower())) + elif token.isdigit() or token.replace('.', '', 1).isdigit() or token.replace('-', '', 1).isdigit(): + stack.append(float(token)) + if isinstance(stack[-1], bool): + return stack.pop() + elif isinstance(stack[-1], int): + return stack.pop() + elif isinstance(stack[-1], tuple): + return stack.pop() + elif stack[-1].is_integer(): + return int(stack.pop()) + else: + return stack.pop() + return calculate(transform_to_polish_notation(exec_isclose(check_expression(parse(math_expression))))) + + +def main(): + try: + parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('EXPRESSION', type=str, help='expression string to evaluate') + args = parser.parse_args() + result = py_calculator(args.EXPRESSION) + print(result) + except Exception as exp: + print(f'ERROR: {exp}') + + +if __name__ == '__main__': + main() diff --git a/final_task/setup.py b/final_task/setup.py index e69de29b..cf863924 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + packages=find_packages(), + description='Pure-python command-line calculator.', + py_modules=['pycalc', 'test'], + entry_points={'console_script': ['pycalc=pycalc:main']} + ) From bcb1401cb6f657e931c5357b54edec3d24606db8 Mon Sep 17 00:00:00 2001 From: Daniil Date: Fri, 10 May 2019 19:02:30 +0300 Subject: [PATCH 02/11] setup fix and renamed module --- final_task/setup.py | 2 +- final_task/{Test.py => tests.py} | 332 ++++++++++++++++--------------- 2 files changed, 168 insertions(+), 166 deletions(-) rename final_task/{Test.py => tests.py} (97%) diff --git a/final_task/setup.py b/final_task/setup.py index cf863924..a6fc4078 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -5,6 +5,6 @@ version='1.0', packages=find_packages(), description='Pure-python command-line calculator.', - py_modules=['pycalc', 'test'], + py_modules=["pycalc", "tests"], entry_points={'console_script': ['pycalc=pycalc:main']} ) diff --git a/final_task/Test.py b/final_task/tests.py similarity index 97% rename from final_task/Test.py rename to final_task/tests.py index b512c8fc..99ab2e22 100644 --- a/final_task/Test.py +++ b/final_task/tests.py @@ -1,165 +1,167 @@ -import unittest -import math -import pycalc - - -class TestMyCaseCalculator(unittest.TestCase): - - def test_default_arithmetic(self): - self.assertEqual(pycalc.py_calculator('10+10'), 10+10) - self.assertEqual(pycalc.py_calculator('2*4'), 2*4) - self.assertEqual(pycalc.py_calculator('5-6'), 5-6) - self.assertEqual(pycalc.py_calculator('10/10'), 10/10) - self.assertEqual(pycalc.py_calculator('10^10'), 10**10) - self.assertEqual(pycalc.py_calculator('21//2'), 21//2) - self.assertEqual(pycalc.py_calculator('2%2'), 2 % 2) - - def test_with_floating_numbers(self): - self.assertEqual(pycalc.py_calculator('0.4 + 1.5'), 0.4 + 1.5) - self.assertEqual(pycalc.py_calculator('.4 + 1.5'), .4 + 1.5) - self.assertEqual(pycalc.py_calculator('.4^-1.5'), eval('0.4**-1.5')) - - def test_number_theoretic_and_representation_functions(self): - self.assertEqual(pycalc.py_calculator('ceil(2.5)'), math.ceil(2.5)) - self.assertEqual(pycalc.py_calculator('copysign(1.0, -1.0)'), math.copysign(1.0, -1.0)) - self.assertEqual(pycalc.py_calculator('fabs(-5)'), math.fabs(-5)) - self.assertEqual(pycalc.py_calculator('factorial(2)'), math.factorial(2)) - self.assertEqual(pycalc.py_calculator('floor(5.5)'), math.floor(5.5)) - self.assertEqual(pycalc.py_calculator('fmod(5,5)'), math.fmod(5, 5)) - self.assertEqual(pycalc.py_calculator('frexp(5)'), math.frexp(5)) - self.assertEqual(pycalc.py_calculator('ldexp(3,10)'), math.ldexp(3, 10)) - self.assertEqual(pycalc.py_calculator('fsum([.1, .1, .1])'), math.fsum([.1, .1, .1])) - self.assertEqual(pycalc.py_calculator('gcd(5, 10)'), math.gcd(5, 10)) - self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05, abs_tol=0.0)'), - math.isclose(1, 2, rel_tol=0.05, abs_tol=0.0)) - self.assertEqual(pycalc.py_calculator('isfinite(3)'), math.isfinite(3)) - self.assertEqual(pycalc.py_calculator('isinf(3)'), math.isinf(3)) - self.assertEqual(pycalc.py_calculator('isnan(3)'), math.isnan(3)) - self.assertEqual(pycalc.py_calculator('modf(-3)'), math.modf(-3)) - self.assertEqual(pycalc.py_calculator('trunc(3.4)'), math.trunc(3.4)) - self.assertEqual(pycalc.py_calculator('exp(3)'), math.exp(3)) - self.assertEqual(pycalc.py_calculator('expm1(3)'), math.expm1(3)) - self.assertEqual(pycalc.py_calculator('log(10,2)'), math.log(10, 2)) - self.assertEqual(pycalc.py_calculator('log1p(10)'), math.log1p(10)) - self.assertEqual(pycalc.py_calculator('log10(10)'), math.log10(10)) - self.assertEqual(pycalc.py_calculator('log2(10)'), math.log2(10)) - self.assertEqual(pycalc.py_calculator('pow(2,3)'), math.pow(2, 3)) - self.assertEqual(pycalc.py_calculator('sqrt(25)'), math.sqrt(25)) - self.assertEqual(pycalc.py_calculator('erf(3)'), math.erf(3)) - self.assertEqual(pycalc.py_calculator('erfc(3)'), math.erfc(3)) - self.assertEqual(pycalc.py_calculator('gamma(3)'), math.gamma(3)) - self.assertEqual(pycalc.py_calculator('lgamma(3)'), math.lgamma(3)) - - def test_module_math_trigonometry(self): - self.assertEqual(pycalc.py_calculator('sin(90)'), math.sin(90)) - self.assertEqual(pycalc.py_calculator('cos(90)'), math.cos(90)) - self.assertEqual(pycalc.py_calculator('tan(90)'), math.tan(90)) - self.assertEqual(pycalc.py_calculator('asin(1)'), math.asin(1)) - self.assertEqual(pycalc.py_calculator('acos(0)'), math.acos(0)) - self.assertEqual(pycalc.py_calculator('atan(1)'), math.atan(1)) - self.assertEqual(pycalc.py_calculator('hypot(3,4)'), math.hypot(3, 4)) - self.assertEqual(pycalc.py_calculator('degrees(3.14)'), math.degrees(3.14)) - self.assertEqual(pycalc.py_calculator('radians(90)'), math.radians(90)) - self.assertEqual(pycalc.py_calculator('sinh(1)'), math.sinh(1)) - self.assertEqual(pycalc.py_calculator('cosh(1)'), math.cosh(1)) - self.assertEqual(pycalc.py_calculator('tanh(1)'), math.tanh(1)) - self.assertEqual(pycalc.py_calculator('asinh(1)'), math.asinh(1)) - self.assertEqual(pycalc.py_calculator('acosh(1)'), math.acosh(1)) - self.assertEqual(pycalc.py_calculator('atanh(0)'), math.atanh(0)) - self.assertEqual(pycalc.py_calculator('pi'), math.pi) - self.assertEqual(pycalc.py_calculator('e'), math.e) - self.assertEqual(pycalc.py_calculator('tau'), math.tau) - - def test_round_brackets(self): - self.assertEqual(pycalc.py_calculator('(2+2)*2'), (2+2)*2) - self.assertEqual(pycalc.py_calculator('(2+2)*2+(2+2)'), (2+2)*2+(2+2)) - self.assertEqual(pycalc.py_calculator('2+(2+(2+3)+3)+2'), 2+(2+(2+3)+3)+2) - self.assertEqual(pycalc.py_calculator('2+(2+3)*3+2'), 2+(2+3)*3+2) - self.assertEqual(pycalc.py_calculator('((2+2)*3)+2'), ((2+2)*3)+2) - - -class TestEpamCaseCalculator(unittest.TestCase): - - def test_unary_operators(self): - self.assertEqual(pycalc.py_calculator("-13"), -13) - self.assertEqual(pycalc.py_calculator("6-(-13)"), 6-(-13)) - self.assertEqual(pycalc.py_calculator("1---1"), 1---1) - self.assertEqual(pycalc.py_calculator("-+---+-1"), -+---+-1) - - def test_operation_priority(self): - self.assertEqual(pycalc.py_calculator("1+2*2"), 1+2*2) - self.assertEqual(pycalc.py_calculator("1+(2+3*2)*3"), 1+(2+3*2)*3) - self.assertEqual(pycalc.py_calculator("10*(2+1)"), 10*(2+1)) - self.assertEqual(pycalc.py_calculator("10^(2+1)"), 10**(2+1)) - self.assertEqual(pycalc.py_calculator("100/3^2"), 100/3**2) - self.assertEqual(pycalc.py_calculator("100/3%2^2"), 100/3 % 2**2) - - def test_functions_and_constants(self): - self.assertEqual(pycalc.py_calculator("pi+e"), math.pi+math.e) - self.assertEqual(pycalc.py_calculator("log(e)"), math.log(math.e)) - self.assertEqual(pycalc.py_calculator("sin(pi/2)"), math.sin(math.pi/2)) - self.assertEqual(pycalc.py_calculator("log10(100)"), math.log10(100)) - self.assertEqual(pycalc.py_calculator("sin(pi/2)*111*6"), math.sin(math.pi/2)*111*6) - self.assertEqual(pycalc.py_calculator("2*sin(pi/2)"), 2*math.sin(math.pi/2)) - self.assertEqual(pycalc.py_calculator("pow(2, 3)"), math.pow(2, 3)) - self.assertEqual(pycalc.py_calculator("abs(-5)"), abs(-5)) - self.assertEqual(pycalc.py_calculator("round(123.4567890)"), round(123.4567890)) - - def test_associative(self): - self.assertEqual(pycalc.py_calculator("102%12%7"), 102 % 12 % 7) - self.assertEqual(pycalc.py_calculator("100/4/3"), 100/4/3) - self.assertEqual(pycalc.py_calculator("2^3^4"), 2**3**4) - - def test_comparison_operators(self): - self.assertEqual(pycalc.py_calculator("1+2*3==1+2*3"), 1+2*3 == 1+2*3) - self.assertAlmostEqual(pycalc.py_calculator("e^5>=e^5+1"), math.e**5 >= math.e**5+1) - self.assertAlmostEqual(pycalc.py_calculator("1+2*4/3+1!=1+2*4/3+2"), 1+2*4/3+1 != 1+2*4/3+2) - - def test_common_tests(self): - self.assertEqual(pycalc.py_calculator("(100)"), eval("(100)")) - self.assertEqual(pycalc.py_calculator("666"), 666) - self.assertEqual(pycalc.py_calculator("-.1"), -.1) - self.assertEqual(pycalc.py_calculator("1/3"), 1/3) - self.assertEqual(pycalc.py_calculator("1.0/3.0"), 1.0/3.0) - self.assertEqual(pycalc.py_calculator(".1 * 2.0^56.0"), .1 * 2.0**56.0) - self.assertEqual(pycalc.py_calculator("e^34"), math.e**34) - self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))"), - (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))) - self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)"), - (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))**(1.0/3.0)) - self.assertEqual(pycalc.py_calculator("sin(pi/2^1) + log(1*4+2^2+1, 3^2)"), - math.sin(math.pi/2**1) + math.log(1*4+2**2+1, 3**2)) - self.assertEqual(pycalc.py_calculator("10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"), - 10*math.e**0*math.log10(.4 - 5 / -0.1-10) - -abs(-53/10) + -5) - expression = "sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+" \ - "cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)" - self.assertEqual(pycalc.py_calculator(expression), - math.sin(-math.cos(-math.sin(3.0)-math.cos(-math.sin(-3.0*5.0)-math.sin(math.cos(math.log10(43.0)))) - + math.cos(math.sin(math.sin(34.0-2.0**2.0))))--math.cos(1.0)--math.cos(0.0)**3.0)) - self.assertEqual(pycalc.py_calculator("2.0^(2.0^2.0*2.0^2.0)"), 2.0**(2.0**2.0*2.0**2.0)) - self.assertEqual(pycalc.py_calculator("sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))"), - math.sin(math.e**math.log(math.e**math.e**math.sin(23.0), 45.0) + - math.cos(3.0+math.log10(math.e**-math.e)))) - - def test_Error_cases(self): - self.assertRaises(ValueError, pycalc.py_calculator, "") - self.assertRaises(ValueError, pycalc.py_calculator, "+") - self.assertRaises(ValueError, pycalc.py_calculator, "1-") - self.assertRaises(ValueError, pycalc.py_calculator, "1 2") - self.assertRaises(ValueError, pycalc.py_calculator, "==7") - self.assertRaises(ValueError, pycalc.py_calculator, "1 + 2(3 * 4))") - self.assertRaises(ValueError, pycalc.py_calculator, "((1+2)") - self.assertRaises(ValueError, pycalc.py_calculator, "1 + 1 2 3 4 5 6 ") - self.assertRaises(ValueError, pycalc.py_calculator, "log100(100)") - self.assertRaises(ValueError, pycalc.py_calculator, "------") - self.assertRaises(ValueError, pycalc.py_calculator, "5 > = 6") - self.assertRaises(ValueError, pycalc.py_calculator, "5 / / 6") - self.assertRaises(ValueError, pycalc.py_calculator, "6 < = 6") - self.assertRaises(ValueError, pycalc.py_calculator, "6 * * 6") - self.assertRaises(ValueError, pycalc.py_calculator, "(((((") - self.assertRaises(ValueError, pycalc.py_calculator, "abs") - self.assertRaises(ValueError, pycalc.py_calculator, "pow(2, 3, 4)") - - -if __name__ == '__main__': - unittest.main() +import unittest +import math +import pycalc + + +class TestMyCaseCalculator(unittest.TestCase): + + def test_default_arithmetic(self): + self.assertEqual(pycalc.py_calculator('10+10'), 10+10) + self.assertEqual(pycalc.py_calculator('2*4'), 2*4) + self.assertEqual(pycalc.py_calculator('5-6'), 5-6) + self.assertEqual(pycalc.py_calculator('10/10'), 10/10) + self.assertEqual(pycalc.py_calculator('10^10'), 10**10) + self.assertEqual(pycalc.py_calculator('21//2'), 21//2) + self.assertEqual(pycalc.py_calculator('2%2'), 2 % 2) + + def test_with_floating_numbers(self): + self.assertEqual(pycalc.py_calculator('0.4 + 1.5'), 0.4 + 1.5) + self.assertEqual(pycalc.py_calculator('.4 + 1.5'), .4 + 1.5) + self.assertEqual(pycalc.py_calculator('.4^-1.5'), eval('0.4**-1.5')) + + def test_number_theoretic_and_representation_functions(self): + self.assertEqual(pycalc.py_calculator('ceil(2.5)'), math.ceil(2.5)) + self.assertEqual(pycalc.py_calculator('copysign(1.0, -1.0)'), math.copysign(1.0, -1.0)) + self.assertEqual(pycalc.py_calculator('fabs(-5)'), math.fabs(-5)) + self.assertEqual(pycalc.py_calculator('factorial(2)'), math.factorial(2)) + self.assertEqual(pycalc.py_calculator('floor(5.5)'), math.floor(5.5)) + self.assertEqual(pycalc.py_calculator('fmod(5,5)'), math.fmod(5, 5)) + self.assertEqual(pycalc.py_calculator('frexp(5)'), math.frexp(5)) + self.assertEqual(pycalc.py_calculator('ldexp(3,10)'), math.ldexp(3, 10)) + self.assertEqual(pycalc.py_calculator('fsum([.1, .1, .1])'), math.fsum([.1, .1, .1])) + self.assertEqual(pycalc.py_calculator('gcd(5, 10)'), math.gcd(5, 10)) + self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05, abs_tol=0.0)'), + math.isclose(1, 2, rel_tol=0.05, abs_tol=0.0)) + self.assertEqual(pycalc.py_calculator('isfinite(3)'), math.isfinite(3)) + self.assertEqual(pycalc.py_calculator('isinf(3)'), math.isinf(3)) + self.assertEqual(pycalc.py_calculator('isnan(3)'), math.isnan(3)) + self.assertEqual(pycalc.py_calculator('modf(-3)'), math.modf(-3)) + self.assertEqual(pycalc.py_calculator('trunc(3.4)'), math.trunc(3.4)) + self.assertEqual(pycalc.py_calculator('exp(3)'), math.exp(3)) + self.assertEqual(pycalc.py_calculator('expm1(3)'), math.expm1(3)) + self.assertEqual(pycalc.py_calculator('log(10,2)'), math.log(10, 2)) + self.assertEqual(pycalc.py_calculator('log1p(10)'), math.log1p(10)) + self.assertEqual(pycalc.py_calculator('log10(10)'), math.log10(10)) + self.assertEqual(pycalc.py_calculator('log2(10)'), math.log2(10)) + self.assertEqual(pycalc.py_calculator('pow(2,3)'), math.pow(2, 3)) + self.assertEqual(pycalc.py_calculator('sqrt(25)'), math.sqrt(25)) + self.assertEqual(pycalc.py_calculator('erf(3)'), math.erf(3)) + self.assertEqual(pycalc.py_calculator('erfc(3)'), math.erfc(3)) + self.assertEqual(pycalc.py_calculator('gamma(3)'), math.gamma(3)) + self.assertEqual(pycalc.py_calculator('lgamma(3)'), math.lgamma(3)) + + def test_module_math_trigonometry(self): + self.assertEqual(pycalc.py_calculator('sin(90)'), math.sin(90)) + self.assertEqual(pycalc.py_calculator('cos(90)'), math.cos(90)) + self.assertEqual(pycalc.py_calculator('tan(90)'), math.tan(90)) + self.assertEqual(pycalc.py_calculator('asin(1)'), math.asin(1)) + self.assertEqual(pycalc.py_calculator('acos(0)'), math.acos(0)) + self.assertEqual(pycalc.py_calculator('atan(1)'), math.atan(1)) + self.assertEqual(pycalc.py_calculator('hypot(3,4)'), math.hypot(3, 4)) + self.assertEqual(pycalc.py_calculator('degrees(3.14)'), math.degrees(3.14)) + self.assertEqual(pycalc.py_calculator('radians(90)'), math.radians(90)) + self.assertEqual(pycalc.py_calculator('sinh(1)'), math.sinh(1)) + self.assertEqual(pycalc.py_calculator('cosh(1)'), math.cosh(1)) + self.assertEqual(pycalc.py_calculator('tanh(1)'), math.tanh(1)) + self.assertEqual(pycalc.py_calculator('asinh(1)'), math.asinh(1)) + self.assertEqual(pycalc.py_calculator('acosh(1)'), math.acosh(1)) + self.assertEqual(pycalc.py_calculator('atanh(0)'), math.atanh(0)) + self.assertEqual(pycalc.py_calculator('pi'), math.pi) + self.assertEqual(pycalc.py_calculator('e'), math.e) + self.assertEqual(pycalc.py_calculator('tau'), math.tau) + + def test_round_brackets(self): + self.assertEqual(pycalc.py_calculator('(2+2)*2'), (2+2)*2) + self.assertEqual(pycalc.py_calculator('(2+2)*2+(2+2)'), (2+2)*2+(2+2)) + self.assertEqual(pycalc.py_calculator('2+(2+(2+3)+3)+2'), 2+(2+(2+3)+3)+2) + self.assertEqual(pycalc.py_calculator('2+(2+3)*3+2'), 2+(2+3)*3+2) + self.assertEqual(pycalc.py_calculator('((2+2)*3)+2'), ((2+2)*3)+2) + + +class TestEpamCaseCalculator(unittest.TestCase): + + def test_unary_operators(self): + self.assertEqual(pycalc.py_calculator("-13"), -13) + self.assertEqual(pycalc.py_calculator("6-(-13)"), 6-(-13)) + self.assertEqual(pycalc.py_calculator("1---1"), 1---1) + self.assertEqual(pycalc.py_calculator("-+---+-1"), -+---+-1) + + def test_operation_priority(self): + self.assertEqual(pycalc.py_calculator("1+2*2"), 1+2*2) + self.assertEqual(pycalc.py_calculator("1+(2+3*2)*3"), 1+(2+3*2)*3) + self.assertEqual(pycalc.py_calculator("10*(2+1)"), 10*(2+1)) + self.assertEqual(pycalc.py_calculator("10^(2+1)"), 10**(2+1)) + self.assertEqual(pycalc.py_calculator("100/3^2"), 100/3**2) + self.assertEqual(pycalc.py_calculator("100/3%2^2"), 100/3 % 2**2) + + def test_functions_and_constants(self): + self.assertEqual(pycalc.py_calculator("pi+e"), math.pi+math.e) + self.assertEqual(pycalc.py_calculator("log(e)"), math.log(math.e)) + self.assertEqual(pycalc.py_calculator("sin(pi/2)"), math.sin(math.pi/2)) + self.assertEqual(pycalc.py_calculator("log10(100)"), math.log10(100)) + self.assertEqual(pycalc.py_calculator("sin(pi/2)*111*6"), math.sin(math.pi/2)*111*6) + self.assertEqual(pycalc.py_calculator("2*sin(pi/2)"), 2*math.sin(math.pi/2)) + self.assertEqual(pycalc.py_calculator("pow(2, 3)"), math.pow(2, 3)) + self.assertEqual(pycalc.py_calculator("abs(-5)"), abs(-5)) + self.assertEqual(pycalc.py_calculator("round(123.4567890)"), round(123.4567890)) + + def test_associative(self): + self.assertEqual(pycalc.py_calculator("102%12%7"), 102 % 12 % 7) + self.assertEqual(pycalc.py_calculator("100/4/3"), 100/4/3) + self.assertEqual(pycalc.py_calculator("2^3^4"), 2**3**4) + + def test_comparison_operators(self): + self.assertEqual(pycalc.py_calculator("1+2*3==1+2*3"), 1+2*3 == 1+2*3) + self.assertAlmostEqual(pycalc.py_calculator("e^5>=e^5+1"), math.e**5 >= math.e**5+1) + self.assertAlmostEqual(pycalc.py_calculator("1+2*4/3+1!=1+2*4/3+2"), 1+2*4/3+1 != 1+2*4/3+2) + + def test_common_tests(self): + self.assertEqual(pycalc.py_calculator("(100)"), eval("(100)")) + self.assertEqual(pycalc.py_calculator("666"), 666) + self.assertEqual(pycalc.py_calculator("-.1"), -.1) + self.assertEqual(pycalc.py_calculator("1/3"), 1/3) + self.assertEqual(pycalc.py_calculator("1.0/3.0"), 1.0/3.0) + self.assertEqual(pycalc.py_calculator(".1 * 2.0^56.0"), .1 * 2.0**56.0) + self.assertEqual(pycalc.py_calculator("e^34"), math.e**34) + self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))"), + (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))) + self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)"), + (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))**(1.0/3.0)) + self.assertEqual(pycalc.py_calculator("sin(pi/2^1) + log(1*4+2^2+1, 3^2)"), + math.sin(math.pi/2**1) + math.log(1*4+2**2+1, 3**2)) + self.assertEqual(pycalc.py_calculator("10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"), + 10*math.e**0*math.log10(.4 - 5 / -0.1-10) - -abs(-53/10) + -5) + expression = "sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+" \ + "cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)" + self.assertEqual(pycalc.py_calculator(expression), + math.sin(-math.cos(-math.sin(3.0)-math.cos(-math.sin(-3.0*5.0) - + math.sin(math.cos(math.log10(43.0)))) + + math.cos(math.sin(math.sin(34.0-2.0**2.0))))--math.cos(1.0) - + -math.cos(0.0)**3.0)) + self.assertEqual(pycalc.py_calculator("2.0^(2.0^2.0*2.0^2.0)"), 2.0**(2.0**2.0*2.0**2.0)) + self.assertEqual(pycalc.py_calculator("sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))"), + math.sin(math.e**math.log(math.e**math.e**math.sin(23.0), 45.0) + + math.cos(3.0+math.log10(math.e**-math.e)))) + + def test_Error_cases(self): + self.assertRaises(ValueError, pycalc.py_calculator, "") + self.assertRaises(ValueError, pycalc.py_calculator, "+") + self.assertRaises(ValueError, pycalc.py_calculator, "1-") + self.assertRaises(ValueError, pycalc.py_calculator, "1 2") + self.assertRaises(ValueError, pycalc.py_calculator, "==7") + self.assertRaises(ValueError, pycalc.py_calculator, "1 + 2(3 * 4))") + self.assertRaises(ValueError, pycalc.py_calculator, "((1+2)") + self.assertRaises(ValueError, pycalc.py_calculator, "1 + 1 2 3 4 5 6 ") + self.assertRaises(ValueError, pycalc.py_calculator, "log100(100)") + self.assertRaises(ValueError, pycalc.py_calculator, "------") + self.assertRaises(ValueError, pycalc.py_calculator, "5 > = 6") + self.assertRaises(ValueError, pycalc.py_calculator, "5 / / 6") + self.assertRaises(ValueError, pycalc.py_calculator, "6 < = 6") + self.assertRaises(ValueError, pycalc.py_calculator, "6 * * 6") + self.assertRaises(ValueError, pycalc.py_calculator, "(((((") + self.assertRaises(ValueError, pycalc.py_calculator, "abs") + self.assertRaises(ValueError, pycalc.py_calculator, "pow(2, 3, 4)") + + +if __name__ == '__main__': + unittest.main() From e0cee9ed11ec9b70248397c391929b2fcd4f2371 Mon Sep 17 00:00:00 2001 From: Daniil Date: Fri, 10 May 2019 19:09:36 +0300 Subject: [PATCH 03/11] setup fix --- final_task/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/setup.py b/final_task/setup.py index a6fc4078..f6caaca0 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -6,5 +6,5 @@ packages=find_packages(), description='Pure-python command-line calculator.', py_modules=["pycalc", "tests"], - entry_points={'console_script': ['pycalc=pycalc:main']} + entry_points={'console_scripts': ['pycalc=pycalc:main']} ) From 3a180e716ee25d47a61d75866ae7edc3ba92a006 Mon Sep 17 00:00:00 2001 From: Daniil Date: Fri, 10 May 2019 21:27:01 +0300 Subject: [PATCH 04/11] added docstring --- final_task/pycalc.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index 3ec1e15f..7aed54e9 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -100,14 +100,13 @@ def parse_step1(expr: str) -> list: """ parse_list = [] - number = '' - function = '' - if bool(re.compile(r'[\d\w()]\s+[.\d\w()]').findall(expr)) \ - or bool(re.compile(r'[+-/*%^=<>]$').findall(expr)) \ - or bool(re.compile(r'^[/*%^=<>!]').findall(expr)) \ - or bool(re.compile(r'[+-/*%^=<>]\)').findall(expr)) \ - or bool(re.compile(r'[<>/*=!]\s+[=/*]').findall(expr)) \ - or bool(re.compile(r'\(\)').findall(expr)): + number, function = '', '' + if re.compile(r'[\d\w()]\s+[.\d\w()]').findall(expr) \ + or re.compile(r'[+-/*%^=<>]$').findall(expr) \ + or re.compile(r'^[/*%^=<>!]').findall(expr) \ + or re.compile(r'[+-/*%^=<>]\)').findall(expr) \ + or re.compile(r'[<>/*=!]\s+[=/*]').findall(expr) \ + or re.compile(r'\(\)').findall(expr): raise ValueError(f'invalid expression') elif not expr: raise ValueError(f'nothing entered') @@ -118,12 +117,9 @@ def parse_step1(expr: str) -> list: number += element parse_list.append(function) function = '' - elif element in OPERATORS or element in LOGIC_OPERATORS or element in "(,)": - parse_list.append(function) - parse_list.append(number) - parse_list.append(element) - number = '' - function = '' + elif element in OPERATORS or element in LOGIC_OPERATORS or element in ['(', ',', ')']: + parse_list.extend([function, number, element]) + number, function = '', '' if number: parse_list.append(number) elif function: @@ -162,7 +158,7 @@ def parse_step3(parse_list: list) -> list: :return: Updated list. """ - for elem in range(len(parse_list)): + for _ in range(len(parse_list)): for index, element in enumerate(parse_list): if element == '-' and parse_list[index + 1] == '-' \ or element == '+' and parse_list[index + 1] == '+': @@ -197,8 +193,8 @@ def parse_step4(parse_list: list) -> list: parse_list[index] = '-1' parse_list.insert(index + 1, '*') except IndexError: - return parse_list - else: + pass + finally: return parse_list return parse_step4(parse_step3(parse_step2(parse_step1(expression)))) @@ -277,7 +273,7 @@ def calc(self): return math.isclose(self.arg1, self.arg2, rel_tol=self.rel_tol, abs_tol=self.abs_tol) def exec_isclose(parse_list): - """ + """calculates the mathematical function isclose :param parse_list: List with items of expression. :return: List with isclose function result. From b71688ef916da65d968687b47b896a93ac1df8e1 Mon Sep 17 00:00:00 2001 From: Daniil Date: Sun, 12 May 2019 21:30:47 +0300 Subject: [PATCH 05/11] add new tests, reworked expression parse. --- final_task/pycalc.py | 189 +++++++++++++++---------------------------- final_task/tests.py | 14 +++- 2 files changed, 79 insertions(+), 124 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index 7aed54e9..62f893e7 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -14,23 +14,16 @@ BUILT_IN_FUNCTION: dictionary of numeric functions; FUNCTIONS: dictionary of mathematical functions; CONSTANTS: dictionary of mathematical constants; - LOGIC_OPERATORS: list of logical operator character; - NEGATIVE_FLOAT_TYPE1: possible representations of a negative float number like -.5; - NEGATIVE_FLOAT_TYPE2: possible representations of a negative float number like -0.5; - NEGATIVE_INTEGER: possible representations of a negative integer number like -5; - FLOAT_TYPE1: possible representations of a float number like 0.5; - FLOAT_TYPE2: possible representations of a float number like .5; - INTEGER: possible representations of a integer number like 5; + ALL_NUMBERS: possible representations of a numbers like -.5, -0.5, -5, 0.5, .5, 5, 1e-05; + INVALID_EXPRESSIONS: list of possible representations of a invalid expression. """ import argparse import math import operator -import string import re from json import loads - OPERATORS = { '+': (operator.add, 2), '-': (operator.sub, 2), @@ -47,14 +40,11 @@ '>=': (operator.ge, 0), '>': (operator.gt, 0), } - BUILT_IN_FUNCTION = { 'abs': abs, 'round': round } - FUNCTIONS = {attr: getattr(math, attr) for attr in dir(math) if callable(getattr(math, attr))} - CONSTANTS = { 'e': math.e, 'pi': math.pi, @@ -66,15 +56,15 @@ '-inf': -math.inf, 'nan': math.inf } - -LOGIC_OPERATORS = ['=', '!', '<', '>'] - -NEGATIVE_FLOAT_TYPE1 = re.compile(r'^-\.\d+$') -NEGATIVE_FLOAT_TYPE2 = re.compile(r'^-\d+\.\d+$') -NEGATIVE_INTEGER = re.compile(r'^-\d+$') -FLOAT_TYPE1 = re.compile(r'^\d+\.\d+$') -FLOAT_TYPE2 = re.compile(r'^\.\d+$') -INTEGER = re.compile(r'^\d+$') +ALL_NUMBERS = re.compile(r'-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?') +INVALID_EXPRESSIONS = [ + r'[\d\w()]\s+[.\d\w()]', + r'[+-/*%^=<>]$', + r'^[/*%^=<>!]', + r'[+-/*%^=<>]\)', + r'[<>/*=!]\s+[=/*]', + r'\(\)' +] def py_calculator(math_expression): @@ -85,7 +75,7 @@ def py_calculator(math_expression): """ def parse(expression: str) -> list: - """Split mathematic expression, consists of four steps + """Split mathematic expression, consists of 3 steps :param expression: String of mathematic expression :return: Parsed line by items in the list @@ -99,59 +89,22 @@ def parse_step1(expr: str) -> list: :raises: ValueError if nothing entered or invalid expression """ - parse_list = [] - number, function = '', '' - if re.compile(r'[\d\w()]\s+[.\d\w()]').findall(expr) \ - or re.compile(r'[+-/*%^=<>]$').findall(expr) \ - or re.compile(r'^[/*%^=<>!]').findall(expr) \ - or re.compile(r'[+-/*%^=<>]\)').findall(expr) \ - or re.compile(r'[<>/*=!]\s+[=/*]').findall(expr) \ - or re.compile(r'\(\)').findall(expr): - raise ValueError(f'invalid expression') - elif not expr: + # this regular expression contains all possible elements of the expression, + # such as numbers, constants, functions, brackets, operators + items_of_expression = r'(?:(?:[a-zA-Z]+[\d]+[\w]?' \ + r'|[a-zA-Z_]+)' \ + r'|(?:\d+(?:\.\d*)?' \ + r'|\.\d+)(?:[eE][-+]?\d+)?' \ + r'|[,+\-/*%^=<>!][=/]?|[(:)])' + for invalid_expr in INVALID_EXPRESSIONS: + if re.search(invalid_expr, expr): + raise ValueError(f'invalid expression') + if not expr: raise ValueError(f'nothing entered') - for element in expr: - if element in string.ascii_letters or element == '_': - function += element - elif element in string.digits or element == '.': - number += element - parse_list.append(function) - function = '' - elif element in OPERATORS or element in LOGIC_OPERATORS or element in ['(', ',', ')']: - parse_list.extend([function, number, element]) - number, function = '', '' - if number: - parse_list.append(number) - elif function: - parse_list.append(function) - return list(filter(None, parse_list)) + parse_list = re.compile(items_of_expression).findall(expr) + return parse_list def parse_step2(parse_list: list) -> list: - """Finishes the parsing functions and collects logical operators and //. - - :param parse_list: List with items of expression - :return: Updated list. - :raise: ValueError if function arguments not entered - - """ - try: - for index, element in enumerate(parse_list): - if element == 'log' and parse_list[index + 1] == 'p' and parse_list[index + 2] == '1': - parse_list[index] += parse_list.pop(index + 2) + parse_list.pop(index + 1) - elif (element in FUNCTIONS or element in BUILT_IN_FUNCTION) and parse_list[index + 1] != '(': - parse_list[index] += parse_list.pop(index + 1) - elif element == 'expm' and parse_list[index + 1] == '1': - parse_list[index] += parse_list.pop(index + 1) - elif element == '/' and parse_list[index + 1] == '/': - parse_list[index] += parse_list.pop(index + 1) - elif element in LOGIC_OPERATORS and parse_list[index + 1] == '=': - parse_list[index] += parse_list.pop(index + 1) - except IndexError: - raise ValueError(f'function arguments not entered') - else: - return parse_list - - def parse_step3(parse_list: list) -> list: """Converts multiple + and -. :param parse_list: List with items of expression. @@ -170,33 +123,33 @@ def parse_step3(parse_list: list) -> list: parse_list.pop(index + 1) return parse_list - def parse_step4(parse_list: list) -> list: + def parse_step3(parse_list: list) -> list: """Working with minuses in expression. :param parse_list: List with items of expression. :return: Updated list. """ - try: - for index, element in enumerate(parse_list): - if element == '-' and (parse_list[index + 1].isdigit() - or parse_list[index + 1].replace('.', '', 1).isdigit() - or parse_list[index + 1] in CONSTANTS): - if parse_list[index - 1] in '(*/%//^,': - parse_list[index] += parse_list.pop(index + 1) - elif parse_list.index(element) == 0: - parse_list[index] += parse_list.pop(index + 1) - elif parse_list[index + 2] in '*/%//': + + for index, element in enumerate(parse_list): + if element == '-' and (re.fullmatch(ALL_NUMBERS, parse_list[index + 1]) + or parse_list[index + 1] in CONSTANTS): + if parse_list[index - 1] in '(*/%//^,': + parse_list[index] += parse_list.pop(index + 1) + elif parse_list.index(element) == 0: + parse_list[index] += parse_list.pop(index + 1) + elif index == len(parse_list) - 2: + if re.search(ALL_NUMBERS, parse_list[index + 1]): parse_list[index] += parse_list.pop(index + 1) parse_list.insert(index, '+') - elif element == '-' and parse_list[index + 1] in FUNCTIONS and parse_list[index - 1] == '(': - parse_list[index] = '-1' - parse_list.insert(index + 1, '*') - except IndexError: - pass - finally: - return parse_list - return parse_step4(parse_step3(parse_step2(parse_step1(expression)))) + elif parse_list[index + 2] in '*/%//' and re.search(ALL_NUMBERS, parse_list[index + 1]): + parse_list[index] += parse_list.pop(index + 1) + parse_list.insert(index, '+') + elif element == '-' and parse_list[index + 1] in FUNCTIONS and parse_list[index - 1] == '(': + parse_list[index] = '-1' + parse_list.insert(index + 1, '*') + return parse_list + return parse_step3(parse_step2(parse_step1(expression))) def check_expression(parse_expression): """Contains functions that validate the input expression. @@ -227,20 +180,18 @@ def check_function_and_constants(parse_list): """ copy_parse_expression = parse_list.copy() for index, element in enumerate(copy_parse_expression): - if element == ''.join(INTEGER.findall(element)) \ - or element == ''.join(FLOAT_TYPE1.findall(element)) \ - or element == ''.join(FLOAT_TYPE2.findall(element)) \ - or element == ''.join(NEGATIVE_INTEGER.findall(element)) \ - or element == ''.join(NEGATIVE_FLOAT_TYPE1.findall(element)) \ - or element == ''.join(NEGATIVE_FLOAT_TYPE2.findall(element)): + if index == len(copy_parse_expression) - 1 and (element in FUNCTIONS or element in BUILT_IN_FUNCTION): + raise ValueError(f'function arguments not entered') + elif (element in FUNCTIONS or element in BUILT_IN_FUNCTION) and copy_parse_expression[index + 1] != '(': + raise ValueError(f'function arguments not entered') + elif re.fullmatch(ALL_NUMBERS, element): copy_parse_expression.pop(index) diff = set(copy_parse_expression).difference( set(FUNCTIONS), set(OPERATORS), set(CONSTANTS), set(BUILT_IN_FUNCTION), - set(string.digits), - {'{', '[', '(', ',', ')', ']', '}'}, + {'{', '[', '(', ',', ':', ')', ']', '}'}, {'True', 'False', 'abs_tol', 'rel_tol'} ) if diff: @@ -283,12 +234,7 @@ def exec_isclose(parse_list): for element in parse_list: if element == 'isclose': for argument in parse_list: - if argument == ''.join(INTEGER.findall(argument)) \ - or argument == ''.join(FLOAT_TYPE1.findall(argument)) \ - or argument == ''.join(FLOAT_TYPE2.findall(argument)) \ - or argument == ''.join(NEGATIVE_INTEGER.findall(argument)) \ - or argument == ''.join(NEGATIVE_FLOAT_TYPE1.findall(argument)) \ - or argument == ''.join(NEGATIVE_FLOAT_TYPE2.findall(argument)): + if re.fullmatch(ALL_NUMBERS, argument): arguments.append(float(argument)) if len(arguments) == 1 or len(arguments) > 4: raise ValueError(f'invalid number of arguments') @@ -315,13 +261,9 @@ def transform_to_polish_notation(parse_expression: list) -> str: reverse_polish_notation = '' separator = ' ' for token in parse_expression: - if token == ''.join(INTEGER.findall(token)) \ - or token == ''.join(FLOAT_TYPE1.findall(token)) \ - or token == ''.join(FLOAT_TYPE2.findall(token)): + if re.fullmatch(ALL_NUMBERS, token) and float(token) >= 0: reverse_polish_notation += token + separator - elif token == ''.join(NEGATIVE_INTEGER.findall(token)) \ - or token == ''.join(NEGATIVE_FLOAT_TYPE1.findall(token)) \ - or token == ''.join(NEGATIVE_FLOAT_TYPE2.findall(token)): + elif re.fullmatch(ALL_NUMBERS, token) and float(token) < 0: # writes a negative number of -5 in the form of 0 5 -, according to the rules of the reverse notation reverse_polish_notation += '0' + separator + token[1:] + separator + token[0] + separator elif token == ')': @@ -336,7 +278,7 @@ def transform_to_polish_notation(parse_expression: list) -> str: break reverse_polish_notation += stack.pop() + separator reverse_polish_notation += token + separator - elif token == '(': + elif token in ['(', ':']: stack.append(token) elif token in FUNCTIONS or token in BUILT_IN_FUNCTION: stack.append(token) @@ -416,14 +358,17 @@ def calculate(reverse_polish_notation): if quantity_of_arguments(FUNCTIONS[token]) == 1: if token == 'fsum': numbers = [] - for number in stack[::-1]: + for index, number in enumerate(stack[::-1]): if isinstance(number, (int, float)) and stack[::-1][1] == ',': - numbers.append(stack.pop()) - stack.pop() - elif stack[::-1][0] == ',' and isinstance(number, (int, float)): - raise ValueError(f'invalid expression') - numbers.append(stack.pop()) - stack.append(numbers) + numbers.append(stack.pop()), stack.pop() + elif len(stack) > index + 2: + if isinstance(stack[::-1][index + 2], (int, float)) and stack[::-1][3] == ',': + numbers.append(stack.pop(-3)), stack.pop(), stack.pop(), stack.pop() + if stack[-1] == ':': + numbers.append(stack.pop(-3)), stack.pop(), stack.pop() + else: + numbers.append(stack.pop()) + stack.extend([numbers]) op = stack.pop() stack.append(FUNCTIONS[token](op)) numbers.clear() @@ -448,13 +393,13 @@ def calculate(reverse_polish_notation): else: op = stack.pop() stack.append(FUNCTIONS[token](op)) - elif token == ',': + elif token in [',', ':']: stack.append(token) elif token in CONSTANTS: stack.append(CONSTANTS[token]) elif token in ('True', 'False'): stack.append(loads(token.lower())) - elif token.isdigit() or token.replace('.', '', 1).isdigit() or token.replace('-', '', 1).isdigit(): + elif re.search(ALL_NUMBERS, token): stack.append(float(token)) if isinstance(stack[-1], bool): return stack.pop() @@ -462,8 +407,6 @@ def calculate(reverse_polish_notation): return stack.pop() elif isinstance(stack[-1], tuple): return stack.pop() - elif stack[-1].is_integer(): - return int(stack.pop()) else: return stack.pop() return calculate(transform_to_polish_notation(exec_isclose(check_expression(parse(math_expression))))) diff --git a/final_task/tests.py b/final_task/tests.py index 99ab2e22..53b5591f 100644 --- a/final_task/tests.py +++ b/final_task/tests.py @@ -17,7 +17,7 @@ def test_default_arithmetic(self): def test_with_floating_numbers(self): self.assertEqual(pycalc.py_calculator('0.4 + 1.5'), 0.4 + 1.5) self.assertEqual(pycalc.py_calculator('.4 + 1.5'), .4 + 1.5) - self.assertEqual(pycalc.py_calculator('.4^-1.5'), eval('0.4**-1.5')) + self.assertEqual(pycalc.py_calculator('.4^-(1.5+0.5)'), eval('.4**-(1.5+0.5)')) def test_number_theoretic_and_representation_functions(self): self.assertEqual(pycalc.py_calculator('ceil(2.5)'), math.ceil(2.5)) @@ -29,9 +29,14 @@ def test_number_theoretic_and_representation_functions(self): self.assertEqual(pycalc.py_calculator('frexp(5)'), math.frexp(5)) self.assertEqual(pycalc.py_calculator('ldexp(3,10)'), math.ldexp(3, 10)) self.assertEqual(pycalc.py_calculator('fsum([.1, .1, .1])'), math.fsum([.1, .1, .1])) + self.assertEqual(pycalc.py_calculator('fsum({1:2, 5:8, 6:120})'), math.fsum({1: 2, 5: 8, 6: 120})) self.assertEqual(pycalc.py_calculator('gcd(5, 10)'), math.gcd(5, 10)) self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05, abs_tol=0.0)'), math.isclose(1, 2, rel_tol=0.05, abs_tol=0.0)) + self.assertEqual(pycalc.py_calculator('isclose(1, 2)'), + math.isclose(1, 2)) + self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05)'), + math.isclose(1, 2, rel_tol=0.05)) self.assertEqual(pycalc.py_calculator('isfinite(3)'), math.isfinite(3)) self.assertEqual(pycalc.py_calculator('isinf(3)'), math.isinf(3)) self.assertEqual(pycalc.py_calculator('isnan(3)'), math.isnan(3)) @@ -104,6 +109,7 @@ def test_functions_and_constants(self): self.assertEqual(pycalc.py_calculator("pow(2, 3)"), math.pow(2, 3)) self.assertEqual(pycalc.py_calculator("abs(-5)"), abs(-5)) self.assertEqual(pycalc.py_calculator("round(123.4567890)"), round(123.4567890)) + self.assertEqual(pycalc.py_calculator("round(123.4567890,2)"), round(123.4567890, 2)) def test_associative(self): self.assertEqual(pycalc.py_calculator("102%12%7"), 102 % 12 % 7) @@ -114,6 +120,7 @@ def test_comparison_operators(self): self.assertEqual(pycalc.py_calculator("1+2*3==1+2*3"), 1+2*3 == 1+2*3) self.assertAlmostEqual(pycalc.py_calculator("e^5>=e^5+1"), math.e**5 >= math.e**5+1) self.assertAlmostEqual(pycalc.py_calculator("1+2*4/3+1!=1+2*4/3+2"), 1+2*4/3+1 != 1+2*4/3+2) + self.assertAlmostEqual(pycalc.py_calculator("True+1"), True + 1) def test_common_tests(self): self.assertEqual(pycalc.py_calculator("(100)"), eval("(100)")) @@ -160,7 +167,12 @@ def test_Error_cases(self): self.assertRaises(ValueError, pycalc.py_calculator, "6 * * 6") self.assertRaises(ValueError, pycalc.py_calculator, "(((((") self.assertRaises(ValueError, pycalc.py_calculator, "abs") + self.assertRaises(ValueError, pycalc.py_calculator, "abs+1") + self.assertRaises(ValueError, pycalc.py_calculator, "isclose(1)") + self.assertRaises(ValueError, pycalc.py_calculator, "cos(2,1)") + self.assertRaises(ValueError, pycalc.py_calculator, "2**2") self.assertRaises(ValueError, pycalc.py_calculator, "pow(2, 3, 4)") + self.assertRaises(ValueError, pycalc.py_calculator, "fsum[1,,2,3]") if __name__ == '__main__': From 52828d1e4523ca4ec48409f3f62b989be7957eaa Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 13 May 2019 21:52:57 +0300 Subject: [PATCH 06/11] redone function that converts multiple + and - --- final_task/pycalc.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index 62f893e7..ee0a4e63 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -75,7 +75,7 @@ def py_calculator(math_expression): """ def parse(expression: str) -> list: - """Split mathematic expression, consists of 3 steps + """Split mathematic expression, consists of 2 steps :param expression: String of mathematic expression :return: Parsed line by items in the list @@ -101,29 +101,16 @@ def parse_step1(expr: str) -> list: raise ValueError(f'invalid expression') if not expr: raise ValueError(f'nothing entered') + expr = expr.replace(' ', '') + while '++' in expr or '--' in expr or '+-' in expr or '-+' in expr: + expr = re.sub(r'\+\+', '+', expr) + expr = re.sub(r'\+-', '-', expr) + expr = re.sub(r'-\+', '-', expr) + expr = re.sub(r'--', '+', expr) parse_list = re.compile(items_of_expression).findall(expr) return parse_list def parse_step2(parse_list: list) -> list: - """Converts multiple + and -. - - :param parse_list: List with items of expression. - :return: Updated list. - - """ - for _ in range(len(parse_list)): - for index, element in enumerate(parse_list): - if element == '-' and parse_list[index + 1] == '-' \ - or element == '+' and parse_list[index + 1] == '+': - parse_list[index] = '+' - parse_list.pop(index + 1) - elif element == '+' and parse_list[index + 1] == '-' \ - or element == '-' and parse_list[index + 1] == '+': - parse_list[index] = '-' - parse_list.pop(index + 1) - return parse_list - - def parse_step3(parse_list: list) -> list: """Working with minuses in expression. :param parse_list: List with items of expression. @@ -149,7 +136,7 @@ def parse_step3(parse_list: list) -> list: parse_list[index] = '-1' parse_list.insert(index + 1, '*') return parse_list - return parse_step3(parse_step2(parse_step1(expression))) + return parse_step2(parse_step1(expression)) def check_expression(parse_expression): """Contains functions that validate the input expression. From dc0942d0a2a799008dcdacdb0cb922751147403e Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 13 May 2019 23:37:54 +0300 Subject: [PATCH 07/11] updated support for type hints --- final_task/pycalc.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index ee0a4e63..0933bdfa 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -24,6 +24,8 @@ import operator import re from json import loads +from typing import List, TypeVar, Union + OPERATORS = { '+': (operator.add, 2), '-': (operator.sub, 2), @@ -67,21 +69,23 @@ ] -def py_calculator(math_expression): +def py_calculator(math_expression: str) -> Union[int, float, bool, tuple]: """ :param math_expression: String of mathematic expression :return: Result of calculation """ - def parse(expression: str) -> list: + elements_of_expression = TypeVar('elements_of_expression') + + def parse(expression: str) -> List[elements_of_expression]: """Split mathematic expression, consists of 2 steps :param expression: String of mathematic expression :return: Parsed line by items in the list """ - def parse_step1(expr: str) -> list: + def parse_step1(expr: str) -> List[elements_of_expression]: """Сreates a list with numbers, functions and constants from a mathematical expression :param expr: String of math expression @@ -110,7 +114,7 @@ def parse_step1(expr: str) -> list: parse_list = re.compile(items_of_expression).findall(expr) return parse_list - def parse_step2(parse_list: list) -> list: + def parse_step2(parse_list: List[elements_of_expression]) -> List[elements_of_expression]: """Working with minuses in expression. :param parse_list: List with items of expression. @@ -138,14 +142,14 @@ def parse_step2(parse_list: list) -> list: return parse_list return parse_step2(parse_step1(expression)) - def check_expression(parse_expression): + def check_expression(parse_expression: List[elements_of_expression]): """Contains functions that validate the input expression. :param parse_expression: List with items of expression :return: List with expression elements if the expression passed all checks """ - def check_operators(parse_list): + def check_operators(parse_list: List[elements_of_expression]): """Checks the validity of the entered operators. :param parse_list: List with items of expression. @@ -158,7 +162,7 @@ def check_operators(parse_list): raise ValueError(f'unknown operation {element + parse_list[index + 1]}') return parse_list - def check_function_and_constants(parse_list): + def check_function_and_constants(parse_list: List[elements_of_expression]): """Checks the validity of the entered functions or constants :param parse_list: List with items of expression. @@ -186,7 +190,7 @@ def check_function_and_constants(parse_list): else: return parse_list - def check_brackets(parse_list): + def check_brackets(parse_list: List[elements_of_expression]): """Check count of '(',')'. :param parse_list: List with items of expression. @@ -210,7 +214,7 @@ def __init__(self, arg1, arg2, arg3=1e-09, arg4=0.0): def calc(self): return math.isclose(self.arg1, self.arg2, rel_tol=self.rel_tol, abs_tol=self.abs_tol) - def exec_isclose(parse_list): + def exec_isclose(parse_list: List[elements_of_expression]): """calculates the mathematical function isclose :param parse_list: List with items of expression. @@ -237,7 +241,7 @@ def exec_isclose(parse_list): else: return parse_list - def transform_to_polish_notation(parse_expression: list) -> str: + def transform_to_polish_notation(parse_expression: List[elements_of_expression]) -> str: """ :param parse_expression: list after parsing @@ -312,7 +316,7 @@ def transform_to_polish_notation(parse_expression: list) -> str: reverse_polish_notation += stack.pop() + separator return reverse_polish_notation - def quantity_of_arguments(function): + def quantity_of_arguments(function: str) -> int: """From the description of the function determines how many arguments it includes :param function: mathematical or numerical functions @@ -323,7 +327,7 @@ def quantity_of_arguments(function): args = spec[spec.find('(') + 1:spec.find(')')] return args.count(',') + 1 if args else 0 - def calculate(reverse_polish_notation): + def calculate(reverse_polish_notation: str) -> Union[int, float, bool, tuple]: stack = [0] for token in reverse_polish_notation.split(' '): if token in OPERATORS: From dd47e9442a3183d2273d19ef296478cefd610991 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 20 May 2019 17:52:52 +0300 Subject: [PATCH 08/11] deleted constants with an additional minus --- final_task/pycalc.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index 0933bdfa..a5314063 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -51,11 +51,7 @@ 'e': math.e, 'pi': math.pi, 'tau': math.tau, - '-e': -math.e, - '-pi': -math.pi, - '-tau': -math.tau, 'inf': math.inf, - '-inf': -math.inf, 'nan': math.inf } ALL_NUMBERS = re.compile(r'-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?') @@ -123,12 +119,9 @@ def parse_step2(parse_list: List[elements_of_expression]) -> List[elements_of_ex """ for index, element in enumerate(parse_list): - if element == '-' and (re.fullmatch(ALL_NUMBERS, parse_list[index + 1]) - or parse_list[index + 1] in CONSTANTS): + if element == '-' and re.fullmatch(ALL_NUMBERS, parse_list[index + 1]): if parse_list[index - 1] in '(*/%//^,': parse_list[index] += parse_list.pop(index + 1) - elif parse_list.index(element) == 0: - parse_list[index] += parse_list.pop(index + 1) elif index == len(parse_list) - 2: if re.search(ALL_NUMBERS, parse_list[index + 1]): parse_list[index] += parse_list.pop(index + 1) From 8fdbd4bdf8951025ad2013a9b1da69930b4252dd Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 20 May 2019 18:50:24 +0300 Subject: [PATCH 09/11] remove the py_calculator enclosing function --- final_task/pycalc.py | 557 +++++++++++++++++++++---------------------- final_task/tests.py | 254 ++++++++++---------- 2 files changed, 405 insertions(+), 406 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index a5314063..4bde0a71 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -3,7 +3,6 @@ The module calculates mathematical expressions. Functions: - py_calculator: consists of 4 stages(parse, check expression, transform to polish notation, calculate); parse: split an expression into numbers, functions, constants, operations; check_expression: validates the expression entered; transform_to_polish_notation: converts expressions according to the rules of reverse polish notation; @@ -65,335 +64,335 @@ ] -def py_calculator(math_expression: str) -> Union[int, float, bool, tuple]: - """ +elements_of_expression = TypeVar('elements_of_expression') - :param math_expression: String of mathematic expression - :return: Result of calculation - """ - elements_of_expression = TypeVar('elements_of_expression') +def parse(expression: str) -> List[elements_of_expression]: + """Split mathematic expression, consists of 2 steps - def parse(expression: str) -> List[elements_of_expression]: - """Split mathematic expression, consists of 2 steps + :param expression: String of mathematic expression + :return: Parsed line by items in the list - :param expression: String of mathematic expression - :return: Parsed line by items in the list + """ + def parse_step1(expr: str) -> List[elements_of_expression]: + """Сreates a list with numbers, functions and constants from a mathematical expression - """ - def parse_step1(expr: str) -> List[elements_of_expression]: - """Сreates a list with numbers, functions and constants from a mathematical expression - - :param expr: String of math expression - :return: List with items of expression - :raises: ValueError if nothing entered or invalid expression - - """ - # this regular expression contains all possible elements of the expression, - # such as numbers, constants, functions, brackets, operators - items_of_expression = r'(?:(?:[a-zA-Z]+[\d]+[\w]?' \ - r'|[a-zA-Z_]+)' \ - r'|(?:\d+(?:\.\d*)?' \ - r'|\.\d+)(?:[eE][-+]?\d+)?' \ - r'|[,+\-/*%^=<>!][=/]?|[(:)])' - for invalid_expr in INVALID_EXPRESSIONS: - if re.search(invalid_expr, expr): - raise ValueError(f'invalid expression') - if not expr: - raise ValueError(f'nothing entered') - expr = expr.replace(' ', '') - while '++' in expr or '--' in expr or '+-' in expr or '-+' in expr: - expr = re.sub(r'\+\+', '+', expr) - expr = re.sub(r'\+-', '-', expr) - expr = re.sub(r'-\+', '-', expr) - expr = re.sub(r'--', '+', expr) - parse_list = re.compile(items_of_expression).findall(expr) - return parse_list + :param expr: String of math expression + :return: List with items of expression + :raises: ValueError if nothing entered or invalid expression - def parse_step2(parse_list: List[elements_of_expression]) -> List[elements_of_expression]: - """Working with minuses in expression. + """ + # this regular expression contains all possible elements of the expression, + # such as numbers, constants, functions, brackets, operators + items_of_expression = r'(?:(?:[a-zA-Z]+[\d]+[\w]?' \ + r'|[a-zA-Z_]+)' \ + r'|(?:\d+(?:\.\d*)?' \ + r'|\.\d+)(?:[eE][-+]?\d+)?' \ + r'|[,+\-/*%^=<>!][=/]?|[(:)])' + for invalid_expr in INVALID_EXPRESSIONS: + if re.search(invalid_expr, expr): + raise ValueError(f'invalid expression') + if not expr: + raise ValueError(f'nothing entered') + expr = expr.replace(' ', '') + while '++' in expr or '--' in expr or '+-' in expr or '-+' in expr: + expr = re.sub(r'\+\+', '+', expr) + expr = re.sub(r'\+-', '-', expr) + expr = re.sub(r'-\+', '-', expr) + expr = re.sub(r'--', '+', expr) + parse_list = re.compile(items_of_expression).findall(expr) + return parse_list + + def parse_step2(parse_list: List[elements_of_expression]) -> List[elements_of_expression]: + """Working with minuses in expression. - :param parse_list: List with items of expression. - :return: Updated list. + :param parse_list: List with items of expression. + :return: Updated list. - """ + """ - for index, element in enumerate(parse_list): - if element == '-' and re.fullmatch(ALL_NUMBERS, parse_list[index + 1]): - if parse_list[index - 1] in '(*/%//^,': - parse_list[index] += parse_list.pop(index + 1) - elif index == len(parse_list) - 2: - if re.search(ALL_NUMBERS, parse_list[index + 1]): - parse_list[index] += parse_list.pop(index + 1) - parse_list.insert(index, '+') - elif parse_list[index + 2] in '*/%//' and re.search(ALL_NUMBERS, parse_list[index + 1]): + for index, element in enumerate(parse_list): + if element == '-' and re.fullmatch(ALL_NUMBERS, parse_list[index + 1]): + if parse_list[index - 1] in '(*/%//^,': + parse_list[index] += parse_list.pop(index + 1) + elif index == len(parse_list) - 2: + if re.search(ALL_NUMBERS, parse_list[index + 1]): parse_list[index] += parse_list.pop(index + 1) parse_list.insert(index, '+') - elif element == '-' and parse_list[index + 1] in FUNCTIONS and parse_list[index - 1] == '(': - parse_list[index] = '-1' - parse_list.insert(index + 1, '*') - return parse_list - return parse_step2(parse_step1(expression)) + elif parse_list[index + 2] in '*/%//' and re.search(ALL_NUMBERS, parse_list[index + 1]): + parse_list[index] += parse_list.pop(index + 1) + parse_list.insert(index, '+') + elif element == '-' and parse_list[index + 1] in FUNCTIONS and parse_list[index - 1] == '(': + parse_list[index] = '-1' + parse_list.insert(index + 1, '*') + return parse_list + return parse_step2(parse_step1(expression)) + + +def check_expression(parse_expression: List[elements_of_expression]): + """Contains functions that validate the input expression. + + :param parse_expression: List with items of expression + :return: List with expression elements if the expression passed all checks - def check_expression(parse_expression: List[elements_of_expression]): - """Contains functions that validate the input expression. + """ + def check_operators(parse_list: List[elements_of_expression]): + """Checks the validity of the entered operators. - :param parse_expression: List with items of expression - :return: List with expression elements if the expression passed all checks + :param parse_list: List with items of expression. + :raise: ValueError if operators follow each other. """ - def check_operators(parse_list: List[elements_of_expression]): - """Checks the validity of the entered operators. - :param parse_list: List with items of expression. - :raise: ValueError if operators follow each other. + for index, element in enumerate(parse_list): + if element in OPERATORS and parse_list[index + 1] in OPERATORS and parse_list[index + 1] != '-': + raise ValueError(f'unknown operation {element + parse_list[index + 1]}') + return parse_list - """ + def check_function_and_constants(parse_list: List[elements_of_expression]): + """Checks the validity of the entered functions or constants + + :param parse_list: List with items of expression. + :raise: ValueError if functions or constants are not supported by a calculator or are incorrectly entered. - for index, element in enumerate(parse_list): - if element in OPERATORS and parse_list[index + 1] in OPERATORS and parse_list[index + 1] != '-': - raise ValueError(f'unknown operation {element + parse_list[index + 1]}') + """ + copy_parse_expression = parse_list.copy() + for index, element in enumerate(copy_parse_expression): + if index == len(copy_parse_expression) - 1 and (element in FUNCTIONS or element in BUILT_IN_FUNCTION): + raise ValueError(f'function arguments not entered') + elif (element in FUNCTIONS or element in BUILT_IN_FUNCTION) and copy_parse_expression[index + 1] != '(': + raise ValueError(f'function arguments not entered') + elif re.fullmatch(ALL_NUMBERS, element): + copy_parse_expression.pop(index) + diff = set(copy_parse_expression).difference( + set(FUNCTIONS), + set(OPERATORS), + set(CONSTANTS), + set(BUILT_IN_FUNCTION), + {'{', '[', '(', ',', ':', ')', ']', '}'}, + {'True', 'False', 'abs_tol', 'rel_tol'} + ) + if diff: + raise ValueError(f'unknown function or constant {diff}') + else: return parse_list - def check_function_and_constants(parse_list: List[elements_of_expression]): - """Checks the validity of the entered functions or constants - - :param parse_list: List with items of expression. - :raise: ValueError if functions or constants are not supported by a calculator or are incorrectly entered. - - """ - copy_parse_expression = parse_list.copy() - for index, element in enumerate(copy_parse_expression): - if index == len(copy_parse_expression) - 1 and (element in FUNCTIONS or element in BUILT_IN_FUNCTION): - raise ValueError(f'function arguments not entered') - elif (element in FUNCTIONS or element in BUILT_IN_FUNCTION) and copy_parse_expression[index + 1] != '(': - raise ValueError(f'function arguments not entered') - elif re.fullmatch(ALL_NUMBERS, element): - copy_parse_expression.pop(index) - diff = set(copy_parse_expression).difference( - set(FUNCTIONS), - set(OPERATORS), - set(CONSTANTS), - set(BUILT_IN_FUNCTION), - {'{', '[', '(', ',', ':', ')', ']', '}'}, - {'True', 'False', 'abs_tol', 'rel_tol'} - ) - if diff: - raise ValueError(f'unknown function or constant {diff}') - else: - return parse_list + def check_brackets(parse_list: List[elements_of_expression]): + """Check count of '(',')'. - def check_brackets(parse_list: List[elements_of_expression]): - """Check count of '(',')'. + :param parse_list: List with items of expression. + :raise: ValueError if Amount '(' not equal to quantity ')' in expression - :param parse_list: List with items of expression. - :raise: ValueError if Amount '(' not equal to quantity ')' in expression + """ + if parse_list.count('(') != parse_list.count(')'): + raise ValueError(f'brackets are not balanced') + else: + return parse_list - """ - if parse_list.count('(') != parse_list.count(')'): - raise ValueError(f'brackets are not balanced') - else: - return parse_list + return check_operators(check_brackets(check_function_and_constants(parse_expression))) - return check_operators(check_brackets(check_function_and_constants(parse_expression))) - class FuncIsclose: - def __init__(self, arg1, arg2, arg3=1e-09, arg4=0.0): - self.arg1 = arg1 - self.arg2 = arg2 - self.rel_tol = arg3 - self.abs_tol = arg4 +class FuncIsclose: + def __init__(self, arg1, arg2, arg3=1e-09, arg4=0.0): + self.arg1 = arg1 + self.arg2 = arg2 + self.rel_tol = arg3 + self.abs_tol = arg4 - def calc(self): - return math.isclose(self.arg1, self.arg2, rel_tol=self.rel_tol, abs_tol=self.abs_tol) + def calc(self): + return math.isclose(self.arg1, self.arg2, rel_tol=self.rel_tol, abs_tol=self.abs_tol) - def exec_isclose(parse_list: List[elements_of_expression]): - """calculates the mathematical function isclose - :param parse_list: List with items of expression. - :return: List with isclose function result. - :raise: ValueError if entered invalid number of arguments. - """ - arguments = [] - for element in parse_list: - if element == 'isclose': - for argument in parse_list: - if re.fullmatch(ALL_NUMBERS, argument): - arguments.append(float(argument)) - if len(arguments) == 1 or len(arguments) > 4: - raise ValueError(f'invalid number of arguments') - elif len(arguments) == 2: - isclose = FuncIsclose(arguments[0], arguments[1]) - return [str(isclose.calc())] - elif len(arguments) == 3: - isclose = FuncIsclose(arguments[0], arguments[1], arguments[2]) - return [str(isclose.calc())] - elif len(arguments) == 4: - isclose = FuncIsclose(arguments[0], arguments[1], arguments[2], arguments[3]) - return [str(isclose.calc())] - else: - return parse_list +def exec_isclose(parse_list: List[elements_of_expression]): + """calculates the mathematical function isclose - def transform_to_polish_notation(parse_expression: List[elements_of_expression]) -> str: - """ + :param parse_list: List with items of expression. + :return: List with isclose function result. + :raise: ValueError if entered invalid number of arguments. + """ + arguments = [] + for element in parse_list: + if element == 'isclose': + for argument in parse_list: + if re.fullmatch(ALL_NUMBERS, argument): + arguments.append(float(argument)) + if len(arguments) == 1 or len(arguments) > 4: + raise ValueError(f'invalid number of arguments') + elif len(arguments) == 2: + isclose = FuncIsclose(arguments[0], arguments[1]) + return [str(isclose.calc())] + elif len(arguments) == 3: + isclose = FuncIsclose(arguments[0], arguments[1], arguments[2]) + return [str(isclose.calc())] + elif len(arguments) == 4: + isclose = FuncIsclose(arguments[0], arguments[1], arguments[2], arguments[3]) + return [str(isclose.calc())] + else: + return parse_list - :param parse_expression: list after parsing - :return: string in reverse polish notation - """ - stack = [] - reverse_polish_notation = '' - separator = ' ' - for token in parse_expression: - if re.fullmatch(ALL_NUMBERS, token) and float(token) >= 0: - reverse_polish_notation += token + separator - elif re.fullmatch(ALL_NUMBERS, token) and float(token) < 0: - # writes a negative number of -5 in the form of 0 5 -, according to the rules of the reverse notation - reverse_polish_notation += '0' + separator + token[1:] + separator + token[0] + separator - elif token == ')': - for element in stack[::-1]: - if element == '(': - break - reverse_polish_notation += stack.pop() + separator - stack.pop() - elif token == ',': - for element in stack[::-1]: - if element == '(': - break - reverse_polish_notation += stack.pop() + separator - reverse_polish_notation += token + separator - elif token in ['(', ':']: +def transform_to_polish_notation(parse_expression: List[elements_of_expression]) -> str: + """ + + :param parse_expression: list after parsing + :return: string in reverse polish notation + + """ + stack = [] + reverse_polish_notation = '' + separator = ' ' + for token in parse_expression: + if re.fullmatch(ALL_NUMBERS, token) and float(token) >= 0: + reverse_polish_notation += token + separator + elif re.fullmatch(ALL_NUMBERS, token) and float(token) < 0: + # writes a negative number of -5 in the form of 0 5 -, according to the rules of the reverse notation + reverse_polish_notation += '0' + separator + token[1:] + separator + token[0] + separator + elif token == ')': + for element in stack[::-1]: + if element == '(': + break + reverse_polish_notation += stack.pop() + separator + stack.pop() + elif token == ',': + for element in stack[::-1]: + if element == '(': + break + reverse_polish_notation += stack.pop() + separator + reverse_polish_notation += token + separator + elif token in ['(', ':']: + stack.append(token) + elif token in FUNCTIONS or token in BUILT_IN_FUNCTION: + stack.append(token) + elif token in OPERATORS: + if not stack: stack.append(token) - elif token in FUNCTIONS or token in BUILT_IN_FUNCTION: + elif token == stack[-1] and token == '^': stack.append(token) - elif token in OPERATORS: + elif stack[-1] == '(': + stack.append(token) + elif stack[-1] in FUNCTIONS or stack[-1] in BUILT_IN_FUNCTION: + reverse_polish_notation += stack.pop() + separator if not stack: stack.append(token) - elif token == stack[-1] and token == '^': - stack.append(token) - elif stack[-1] == '(': + elif stack[-1] in OPERATORS: + if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + stack.append(token) + elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: + stack.append(token) + elif token == '-' and parse_expression[parse_expression.index(token) - 1] in '/*^%//': + if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: stack.append(token) - elif stack[-1] in FUNCTIONS or stack[-1] in BUILT_IN_FUNCTION: - reverse_polish_notation += stack.pop() + separator - if not stack: + reverse_polish_notation += '0' + separator + elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + if stack: + if stack[-1] == '(': stack.append(token) - elif stack[-1] in OPERATORS: - if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - reverse_polish_notation += stack.pop() + separator - stack.append(token) - elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: - stack.append(token) - elif token == '-' and parse_expression[parse_expression.index(token) - 1] in '/*^%//': - if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator stack.append(token) - reverse_polish_notation += '0' + separator - elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - reverse_polish_notation += stack.pop() + separator - if stack: - if stack[-1] == '(': - stack.append(token) - elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - reverse_polish_notation += stack.pop() + separator - stack.append(token) - elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: - stack.append(token) - elif not stack: + elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: stack.append(token) - else: + elif not stack: stack.append(token) - elif token in CONSTANTS: - reverse_polish_notation += token + separator - elif token in ('True', 'False'): - reverse_polish_notation += token + separator - while stack: - reverse_polish_notation += stack.pop() + separator - return reverse_polish_notation + else: + stack.append(token) + elif token in CONSTANTS: + reverse_polish_notation += token + separator + elif token in ('True', 'False'): + reverse_polish_notation += token + separator + while stack: + reverse_polish_notation += stack.pop() + separator + return reverse_polish_notation - def quantity_of_arguments(function: str) -> int: - """From the description of the function determines how many arguments it includes - :param function: mathematical or numerical functions - :return: quantity of arguments +def quantity_of_arguments(function: str) -> int: + """From the description of the function determines how many arguments it includes - """ - spec = function.__doc__.split('\n')[0] - args = spec[spec.find('(') + 1:spec.find(')')] - return args.count(',') + 1 if args else 0 - - def calculate(reverse_polish_notation: str) -> Union[int, float, bool, tuple]: - stack = [0] - for token in reverse_polish_notation.split(' '): - if token in OPERATORS: - op2, op1 = stack.pop(), stack.pop() - stack.append(OPERATORS[token][0](op1, op2)) - elif token in BUILT_IN_FUNCTION: + :param function: mathematical or numerical functions + :return: quantity of arguments + + """ + spec = function.__doc__.split('\n')[0] + args = spec[spec.find('(') + 1:spec.find(')')] + return args.count(',') + 1 if args else 0 + + +def calculate(math_expression: str) -> Union[int, float, bool, tuple]: + reverse_polish_notation = transform_to_polish_notation(exec_isclose(check_expression(parse(math_expression)))) + stack = [0] + for token in reverse_polish_notation.split(' '): + if token in OPERATORS: + op2, op1 = stack.pop(), stack.pop() + stack.append(OPERATORS[token][0](op1, op2)) + elif token in BUILT_IN_FUNCTION: + if stack[-2] == ',': + op2 = stack.pop() + stack.pop() + op1 = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + else: + stack.append(BUILT_IN_FUNCTION[token](op1, int(op2))) + else: + op = stack.pop() + stack.append(BUILT_IN_FUNCTION[token](op)) + elif token in FUNCTIONS: + if quantity_of_arguments(FUNCTIONS[token]) == 1: + if token == 'fsum': + numbers = [] + for index, number in enumerate(stack[::-1]): + if isinstance(number, (int, float)) and stack[::-1][1] == ',': + numbers.append(stack.pop()), stack.pop() + elif len(stack) > index + 2: + if isinstance(stack[::-1][index + 2], (int, float)) and stack[::-1][3] == ',': + numbers.append(stack.pop(-3)), stack.pop(), stack.pop(), stack.pop() + if stack[-1] == ':': + numbers.append(stack.pop(-3)), stack.pop(), stack.pop() + else: + numbers.append(stack.pop()) + stack.extend([numbers]) + op = stack.pop() + stack.append(FUNCTIONS[token](op)) + numbers.clear() + else: + op = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + stack.append(FUNCTIONS[token](op)) + elif quantity_of_arguments(FUNCTIONS[token]) == 2: if stack[-2] == ',': op2 = stack.pop() stack.pop() op1 = stack.pop() if stack[-1] == ',': raise ValueError(f'invalid number of arguments') + elif token == 'ldexp': + stack.append(FUNCTIONS[token](op1, int(op2))) + elif token == 'gcd': + stack.append(FUNCTIONS[token](int(op1), int(op2))) else: - stack.append(BUILT_IN_FUNCTION[token](op1, int(op2))) + stack.append(FUNCTIONS[token](op1, op2)) else: op = stack.pop() - stack.append(BUILT_IN_FUNCTION[token](op)) - elif token in FUNCTIONS: - if quantity_of_arguments(FUNCTIONS[token]) == 1: - if token == 'fsum': - numbers = [] - for index, number in enumerate(stack[::-1]): - if isinstance(number, (int, float)) and stack[::-1][1] == ',': - numbers.append(stack.pop()), stack.pop() - elif len(stack) > index + 2: - if isinstance(stack[::-1][index + 2], (int, float)) and stack[::-1][3] == ',': - numbers.append(stack.pop(-3)), stack.pop(), stack.pop(), stack.pop() - if stack[-1] == ':': - numbers.append(stack.pop(-3)), stack.pop(), stack.pop() - else: - numbers.append(stack.pop()) - stack.extend([numbers]) - op = stack.pop() - stack.append(FUNCTIONS[token](op)) - numbers.clear() - else: - op = stack.pop() - if stack[-1] == ',': - raise ValueError(f'invalid number of arguments') - stack.append(FUNCTIONS[token](op)) - elif quantity_of_arguments(FUNCTIONS[token]) == 2: - if stack[-2] == ',': - op2 = stack.pop() - stack.pop() - op1 = stack.pop() - if stack[-1] == ',': - raise ValueError(f'invalid number of arguments') - elif token == 'ldexp': - stack.append(FUNCTIONS[token](op1, int(op2))) - elif token == 'gcd': - stack.append(FUNCTIONS[token](int(op1), int(op2))) - else: - stack.append(FUNCTIONS[token](op1, op2)) - else: - op = stack.pop() - stack.append(FUNCTIONS[token](op)) - elif token in [',', ':']: - stack.append(token) - elif token in CONSTANTS: - stack.append(CONSTANTS[token]) - elif token in ('True', 'False'): - stack.append(loads(token.lower())) - elif re.search(ALL_NUMBERS, token): - stack.append(float(token)) - if isinstance(stack[-1], bool): - return stack.pop() - elif isinstance(stack[-1], int): - return stack.pop() - elif isinstance(stack[-1], tuple): - return stack.pop() - else: - return stack.pop() - return calculate(transform_to_polish_notation(exec_isclose(check_expression(parse(math_expression))))) + stack.append(FUNCTIONS[token](op)) + elif token in [',', ':']: + stack.append(token) + elif token in CONSTANTS: + stack.append(CONSTANTS[token]) + elif token in ('True', 'False'): + stack.append(loads(token.lower())) + elif re.search(ALL_NUMBERS, token): + stack.append(float(token)) + if isinstance(stack[-1], bool): + return stack.pop() + elif isinstance(stack[-1], int): + return stack.pop() + elif isinstance(stack[-1], tuple): + return stack.pop() + else: + return stack.pop() def main(): @@ -401,7 +400,7 @@ def main(): parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') parser.add_argument('EXPRESSION', type=str, help='expression string to evaluate') args = parser.parse_args() - result = py_calculator(args.EXPRESSION) + result = calculate(args.EXPRESSION) print(result) except Exception as exp: print(f'ERROR: {exp}') diff --git a/final_task/tests.py b/final_task/tests.py index 53b5591f..e4fc757c 100644 --- a/final_task/tests.py +++ b/final_task/tests.py @@ -6,173 +6,173 @@ class TestMyCaseCalculator(unittest.TestCase): def test_default_arithmetic(self): - self.assertEqual(pycalc.py_calculator('10+10'), 10+10) - self.assertEqual(pycalc.py_calculator('2*4'), 2*4) - self.assertEqual(pycalc.py_calculator('5-6'), 5-6) - self.assertEqual(pycalc.py_calculator('10/10'), 10/10) - self.assertEqual(pycalc.py_calculator('10^10'), 10**10) - self.assertEqual(pycalc.py_calculator('21//2'), 21//2) - self.assertEqual(pycalc.py_calculator('2%2'), 2 % 2) + self.assertEqual(pycalc.calculate('10+10'), 10+10) + self.assertEqual(pycalc.calculate('2*4'), 2*4) + self.assertEqual(pycalc.calculate('5-6'), 5-6) + self.assertEqual(pycalc.calculate('10/10'), 10/10) + self.assertEqual(pycalc.calculate('10^10'), 10**10) + self.assertEqual(pycalc.calculate('21//2'), 21//2) + self.assertEqual(pycalc.calculate('2%2'), 2 % 2) def test_with_floating_numbers(self): - self.assertEqual(pycalc.py_calculator('0.4 + 1.5'), 0.4 + 1.5) - self.assertEqual(pycalc.py_calculator('.4 + 1.5'), .4 + 1.5) - self.assertEqual(pycalc.py_calculator('.4^-(1.5+0.5)'), eval('.4**-(1.5+0.5)')) + self.assertEqual(pycalc.calculate('0.4 + 1.5'), 0.4 + 1.5) + self.assertEqual(pycalc.calculate('.4 + 1.5'), .4 + 1.5) + self.assertEqual(pycalc.calculate('.4^-(1.5+0.5)'), eval('.4**-(1.5+0.5)')) def test_number_theoretic_and_representation_functions(self): - self.assertEqual(pycalc.py_calculator('ceil(2.5)'), math.ceil(2.5)) - self.assertEqual(pycalc.py_calculator('copysign(1.0, -1.0)'), math.copysign(1.0, -1.0)) - self.assertEqual(pycalc.py_calculator('fabs(-5)'), math.fabs(-5)) - self.assertEqual(pycalc.py_calculator('factorial(2)'), math.factorial(2)) - self.assertEqual(pycalc.py_calculator('floor(5.5)'), math.floor(5.5)) - self.assertEqual(pycalc.py_calculator('fmod(5,5)'), math.fmod(5, 5)) - self.assertEqual(pycalc.py_calculator('frexp(5)'), math.frexp(5)) - self.assertEqual(pycalc.py_calculator('ldexp(3,10)'), math.ldexp(3, 10)) - self.assertEqual(pycalc.py_calculator('fsum([.1, .1, .1])'), math.fsum([.1, .1, .1])) - self.assertEqual(pycalc.py_calculator('fsum({1:2, 5:8, 6:120})'), math.fsum({1: 2, 5: 8, 6: 120})) - self.assertEqual(pycalc.py_calculator('gcd(5, 10)'), math.gcd(5, 10)) - self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05, abs_tol=0.0)'), + self.assertEqual(pycalc.calculate('ceil(2.5)'), math.ceil(2.5)) + self.assertEqual(pycalc.calculate('copysign(1.0, -1.0)'), math.copysign(1.0, -1.0)) + self.assertEqual(pycalc.calculate('fabs(-5)'), math.fabs(-5)) + self.assertEqual(pycalc.calculate('factorial(2)'), math.factorial(2)) + self.assertEqual(pycalc.calculate('floor(5.5)'), math.floor(5.5)) + self.assertEqual(pycalc.calculate('fmod(5,5)'), math.fmod(5, 5)) + self.assertEqual(pycalc.calculate('frexp(5)'), math.frexp(5)) + self.assertEqual(pycalc.calculate('ldexp(3,10)'), math.ldexp(3, 10)) + self.assertEqual(pycalc.calculate('fsum([.1, .1, .1])'), math.fsum([.1, .1, .1])) + self.assertEqual(pycalc.calculate('fsum({1:2, 5:8, 6:120})'), math.fsum({1: 2, 5: 8, 6: 120})) + self.assertEqual(pycalc.calculate('gcd(5, 10)'), math.gcd(5, 10)) + self.assertEqual(pycalc.calculate('isclose(1, 2, rel_tol=0.05, abs_tol=0.0)'), math.isclose(1, 2, rel_tol=0.05, abs_tol=0.0)) - self.assertEqual(pycalc.py_calculator('isclose(1, 2)'), + self.assertEqual(pycalc.calculate('isclose(1, 2)'), math.isclose(1, 2)) - self.assertEqual(pycalc.py_calculator('isclose(1, 2, rel_tol=0.05)'), + self.assertEqual(pycalc.calculate('isclose(1, 2, rel_tol=0.05)'), math.isclose(1, 2, rel_tol=0.05)) - self.assertEqual(pycalc.py_calculator('isfinite(3)'), math.isfinite(3)) - self.assertEqual(pycalc.py_calculator('isinf(3)'), math.isinf(3)) - self.assertEqual(pycalc.py_calculator('isnan(3)'), math.isnan(3)) - self.assertEqual(pycalc.py_calculator('modf(-3)'), math.modf(-3)) - self.assertEqual(pycalc.py_calculator('trunc(3.4)'), math.trunc(3.4)) - self.assertEqual(pycalc.py_calculator('exp(3)'), math.exp(3)) - self.assertEqual(pycalc.py_calculator('expm1(3)'), math.expm1(3)) - self.assertEqual(pycalc.py_calculator('log(10,2)'), math.log(10, 2)) - self.assertEqual(pycalc.py_calculator('log1p(10)'), math.log1p(10)) - self.assertEqual(pycalc.py_calculator('log10(10)'), math.log10(10)) - self.assertEqual(pycalc.py_calculator('log2(10)'), math.log2(10)) - self.assertEqual(pycalc.py_calculator('pow(2,3)'), math.pow(2, 3)) - self.assertEqual(pycalc.py_calculator('sqrt(25)'), math.sqrt(25)) - self.assertEqual(pycalc.py_calculator('erf(3)'), math.erf(3)) - self.assertEqual(pycalc.py_calculator('erfc(3)'), math.erfc(3)) - self.assertEqual(pycalc.py_calculator('gamma(3)'), math.gamma(3)) - self.assertEqual(pycalc.py_calculator('lgamma(3)'), math.lgamma(3)) + self.assertEqual(pycalc.calculate('isfinite(3)'), math.isfinite(3)) + self.assertEqual(pycalc.calculate('isinf(3)'), math.isinf(3)) + self.assertEqual(pycalc.calculate('isnan(3)'), math.isnan(3)) + self.assertEqual(pycalc.calculate('modf(-3)'), math.modf(-3)) + self.assertEqual(pycalc.calculate('trunc(3.4)'), math.trunc(3.4)) + self.assertEqual(pycalc.calculate('exp(3)'), math.exp(3)) + self.assertEqual(pycalc.calculate('expm1(3)'), math.expm1(3)) + self.assertEqual(pycalc.calculate('log(10,2)'), math.log(10, 2)) + self.assertEqual(pycalc.calculate('log1p(10)'), math.log1p(10)) + self.assertEqual(pycalc.calculate('log10(10)'), math.log10(10)) + self.assertEqual(pycalc.calculate('log2(10)'), math.log2(10)) + self.assertEqual(pycalc.calculate('pow(2,3)'), math.pow(2, 3)) + self.assertEqual(pycalc.calculate('sqrt(25)'), math.sqrt(25)) + self.assertEqual(pycalc.calculate('erf(3)'), math.erf(3)) + self.assertEqual(pycalc.calculate('erfc(3)'), math.erfc(3)) + self.assertEqual(pycalc.calculate('gamma(3)'), math.gamma(3)) + self.assertEqual(pycalc.calculate('lgamma(3)'), math.lgamma(3)) def test_module_math_trigonometry(self): - self.assertEqual(pycalc.py_calculator('sin(90)'), math.sin(90)) - self.assertEqual(pycalc.py_calculator('cos(90)'), math.cos(90)) - self.assertEqual(pycalc.py_calculator('tan(90)'), math.tan(90)) - self.assertEqual(pycalc.py_calculator('asin(1)'), math.asin(1)) - self.assertEqual(pycalc.py_calculator('acos(0)'), math.acos(0)) - self.assertEqual(pycalc.py_calculator('atan(1)'), math.atan(1)) - self.assertEqual(pycalc.py_calculator('hypot(3,4)'), math.hypot(3, 4)) - self.assertEqual(pycalc.py_calculator('degrees(3.14)'), math.degrees(3.14)) - self.assertEqual(pycalc.py_calculator('radians(90)'), math.radians(90)) - self.assertEqual(pycalc.py_calculator('sinh(1)'), math.sinh(1)) - self.assertEqual(pycalc.py_calculator('cosh(1)'), math.cosh(1)) - self.assertEqual(pycalc.py_calculator('tanh(1)'), math.tanh(1)) - self.assertEqual(pycalc.py_calculator('asinh(1)'), math.asinh(1)) - self.assertEqual(pycalc.py_calculator('acosh(1)'), math.acosh(1)) - self.assertEqual(pycalc.py_calculator('atanh(0)'), math.atanh(0)) - self.assertEqual(pycalc.py_calculator('pi'), math.pi) - self.assertEqual(pycalc.py_calculator('e'), math.e) - self.assertEqual(pycalc.py_calculator('tau'), math.tau) + self.assertEqual(pycalc.calculate('sin(90)'), math.sin(90)) + self.assertEqual(pycalc.calculate('cos(90)'), math.cos(90)) + self.assertEqual(pycalc.calculate('tan(90)'), math.tan(90)) + self.assertEqual(pycalc.calculate('asin(1)'), math.asin(1)) + self.assertEqual(pycalc.calculate('acos(0)'), math.acos(0)) + self.assertEqual(pycalc.calculate('atan(1)'), math.atan(1)) + self.assertEqual(pycalc.calculate('hypot(3,4)'), math.hypot(3, 4)) + self.assertEqual(pycalc.calculate('degrees(3.14)'), math.degrees(3.14)) + self.assertEqual(pycalc.calculate('radians(90)'), math.radians(90)) + self.assertEqual(pycalc.calculate('sinh(1)'), math.sinh(1)) + self.assertEqual(pycalc.calculate('cosh(1)'), math.cosh(1)) + self.assertEqual(pycalc.calculate('tanh(1)'), math.tanh(1)) + self.assertEqual(pycalc.calculate('asinh(1)'), math.asinh(1)) + self.assertEqual(pycalc.calculate('acosh(1)'), math.acosh(1)) + self.assertEqual(pycalc.calculate('atanh(0)'), math.atanh(0)) + self.assertEqual(pycalc.calculate('pi'), math.pi) + self.assertEqual(pycalc.calculate('e'), math.e) + self.assertEqual(pycalc.calculate('tau'), math.tau) def test_round_brackets(self): - self.assertEqual(pycalc.py_calculator('(2+2)*2'), (2+2)*2) - self.assertEqual(pycalc.py_calculator('(2+2)*2+(2+2)'), (2+2)*2+(2+2)) - self.assertEqual(pycalc.py_calculator('2+(2+(2+3)+3)+2'), 2+(2+(2+3)+3)+2) - self.assertEqual(pycalc.py_calculator('2+(2+3)*3+2'), 2+(2+3)*3+2) - self.assertEqual(pycalc.py_calculator('((2+2)*3)+2'), ((2+2)*3)+2) + self.assertEqual(pycalc.calculate('(2+2)*2'), (2+2)*2) + self.assertEqual(pycalc.calculate('(2+2)*2+(2+2)'), (2+2)*2+(2+2)) + self.assertEqual(pycalc.calculate('2+(2+(2+3)+3)+2'), 2+(2+(2+3)+3)+2) + self.assertEqual(pycalc.calculate('2+(2+3)*3+2'), 2+(2+3)*3+2) + self.assertEqual(pycalc.calculate('((2+2)*3)+2'), ((2+2)*3)+2) class TestEpamCaseCalculator(unittest.TestCase): def test_unary_operators(self): - self.assertEqual(pycalc.py_calculator("-13"), -13) - self.assertEqual(pycalc.py_calculator("6-(-13)"), 6-(-13)) - self.assertEqual(pycalc.py_calculator("1---1"), 1---1) - self.assertEqual(pycalc.py_calculator("-+---+-1"), -+---+-1) + self.assertEqual(pycalc.calculate("-13"), -13) + self.assertEqual(pycalc.calculate("6-(-13)"), 6-(-13)) + self.assertEqual(pycalc.calculate("1---1"), 1---1) + self.assertEqual(pycalc.calculate("-+---+-1"), -+---+-1) def test_operation_priority(self): - self.assertEqual(pycalc.py_calculator("1+2*2"), 1+2*2) - self.assertEqual(pycalc.py_calculator("1+(2+3*2)*3"), 1+(2+3*2)*3) - self.assertEqual(pycalc.py_calculator("10*(2+1)"), 10*(2+1)) - self.assertEqual(pycalc.py_calculator("10^(2+1)"), 10**(2+1)) - self.assertEqual(pycalc.py_calculator("100/3^2"), 100/3**2) - self.assertEqual(pycalc.py_calculator("100/3%2^2"), 100/3 % 2**2) + self.assertEqual(pycalc.calculate("1+2*2"), 1+2*2) + self.assertEqual(pycalc.calculate("1+(2+3*2)*3"), 1+(2+3*2)*3) + self.assertEqual(pycalc.calculate("10*(2+1)"), 10*(2+1)) + self.assertEqual(pycalc.calculate("10^(2+1)"), 10**(2+1)) + self.assertEqual(pycalc.calculate("100/3^2"), 100/3**2) + self.assertEqual(pycalc.calculate("100/3%2^2"), 100/3 % 2**2) def test_functions_and_constants(self): - self.assertEqual(pycalc.py_calculator("pi+e"), math.pi+math.e) - self.assertEqual(pycalc.py_calculator("log(e)"), math.log(math.e)) - self.assertEqual(pycalc.py_calculator("sin(pi/2)"), math.sin(math.pi/2)) - self.assertEqual(pycalc.py_calculator("log10(100)"), math.log10(100)) - self.assertEqual(pycalc.py_calculator("sin(pi/2)*111*6"), math.sin(math.pi/2)*111*6) - self.assertEqual(pycalc.py_calculator("2*sin(pi/2)"), 2*math.sin(math.pi/2)) - self.assertEqual(pycalc.py_calculator("pow(2, 3)"), math.pow(2, 3)) - self.assertEqual(pycalc.py_calculator("abs(-5)"), abs(-5)) - self.assertEqual(pycalc.py_calculator("round(123.4567890)"), round(123.4567890)) - self.assertEqual(pycalc.py_calculator("round(123.4567890,2)"), round(123.4567890, 2)) + self.assertEqual(pycalc.calculate("pi+e"), math.pi+math.e) + self.assertEqual(pycalc.calculate("log(e)"), math.log(math.e)) + self.assertEqual(pycalc.calculate("sin(pi/2)"), math.sin(math.pi/2)) + self.assertEqual(pycalc.calculate("log10(100)"), math.log10(100)) + self.assertEqual(pycalc.calculate("sin(pi/2)*111*6"), math.sin(math.pi/2)*111*6) + self.assertEqual(pycalc.calculate("2*sin(pi/2)"), 2*math.sin(math.pi/2)) + self.assertEqual(pycalc.calculate("pow(2, 3)"), math.pow(2, 3)) + self.assertEqual(pycalc.calculate("abs(-5)"), abs(-5)) + self.assertEqual(pycalc.calculate("round(123.4567890)"), round(123.4567890)) + self.assertEqual(pycalc.calculate("round(123.4567890,2)"), round(123.4567890, 2)) def test_associative(self): - self.assertEqual(pycalc.py_calculator("102%12%7"), 102 % 12 % 7) - self.assertEqual(pycalc.py_calculator("100/4/3"), 100/4/3) - self.assertEqual(pycalc.py_calculator("2^3^4"), 2**3**4) + self.assertEqual(pycalc.calculate("102%12%7"), 102 % 12 % 7) + self.assertEqual(pycalc.calculate("100/4/3"), 100/4/3) + self.assertEqual(pycalc.calculate("2^3^4"), 2**3**4) def test_comparison_operators(self): - self.assertEqual(pycalc.py_calculator("1+2*3==1+2*3"), 1+2*3 == 1+2*3) - self.assertAlmostEqual(pycalc.py_calculator("e^5>=e^5+1"), math.e**5 >= math.e**5+1) - self.assertAlmostEqual(pycalc.py_calculator("1+2*4/3+1!=1+2*4/3+2"), 1+2*4/3+1 != 1+2*4/3+2) - self.assertAlmostEqual(pycalc.py_calculator("True+1"), True + 1) + self.assertEqual(pycalc.calculate("1+2*3==1+2*3"), 1+2*3 == 1+2*3) + self.assertAlmostEqual(pycalc.calculate("e^5>=e^5+1"), math.e**5 >= math.e**5+1) + self.assertAlmostEqual(pycalc.calculate("1+2*4/3+1!=1+2*4/3+2"), 1+2*4/3+1 != 1+2*4/3+2) + self.assertAlmostEqual(pycalc.calculate("True+1"), True + 1) def test_common_tests(self): - self.assertEqual(pycalc.py_calculator("(100)"), eval("(100)")) - self.assertEqual(pycalc.py_calculator("666"), 666) - self.assertEqual(pycalc.py_calculator("-.1"), -.1) - self.assertEqual(pycalc.py_calculator("1/3"), 1/3) - self.assertEqual(pycalc.py_calculator("1.0/3.0"), 1.0/3.0) - self.assertEqual(pycalc.py_calculator(".1 * 2.0^56.0"), .1 * 2.0**56.0) - self.assertEqual(pycalc.py_calculator("e^34"), math.e**34) - self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))"), + self.assertEqual(pycalc.calculate("(100)"), eval("(100)")) + self.assertEqual(pycalc.calculate("666"), 666) + self.assertEqual(pycalc.calculate("-.1"), -.1) + self.assertEqual(pycalc.calculate("1/3"), 1/3) + self.assertEqual(pycalc.calculate("1.0/3.0"), 1.0/3.0) + self.assertEqual(pycalc.calculate(".1 * 2.0^56.0"), .1 * 2.0**56.0) + self.assertEqual(pycalc.calculate("e^34"), math.e**34) + self.assertEqual(pycalc.calculate("(2.0^(pi/pi+e/e+2.0^0.0))"), (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))) - self.assertEqual(pycalc.py_calculator("(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)"), + self.assertEqual(pycalc.calculate("(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)"), (2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))**(1.0/3.0)) - self.assertEqual(pycalc.py_calculator("sin(pi/2^1) + log(1*4+2^2+1, 3^2)"), + self.assertEqual(pycalc.calculate("sin(pi/2^1) + log(1*4+2^2+1, 3^2)"), math.sin(math.pi/2**1) + math.log(1*4+2**2+1, 3**2)) - self.assertEqual(pycalc.py_calculator("10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"), + self.assertEqual(pycalc.calculate("10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"), 10*math.e**0*math.log10(.4 - 5 / -0.1-10) - -abs(-53/10) + -5) expression = "sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+" \ "cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)" - self.assertEqual(pycalc.py_calculator(expression), + self.assertEqual(pycalc.calculate(expression), math.sin(-math.cos(-math.sin(3.0)-math.cos(-math.sin(-3.0*5.0) - math.sin(math.cos(math.log10(43.0)))) + math.cos(math.sin(math.sin(34.0-2.0**2.0))))--math.cos(1.0) - -math.cos(0.0)**3.0)) - self.assertEqual(pycalc.py_calculator("2.0^(2.0^2.0*2.0^2.0)"), 2.0**(2.0**2.0*2.0**2.0)) - self.assertEqual(pycalc.py_calculator("sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))"), + self.assertEqual(pycalc.calculate("2.0^(2.0^2.0*2.0^2.0)"), 2.0**(2.0**2.0*2.0**2.0)) + self.assertEqual(pycalc.calculate("sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))"), math.sin(math.e**math.log(math.e**math.e**math.sin(23.0), 45.0) + math.cos(3.0+math.log10(math.e**-math.e)))) def test_Error_cases(self): - self.assertRaises(ValueError, pycalc.py_calculator, "") - self.assertRaises(ValueError, pycalc.py_calculator, "+") - self.assertRaises(ValueError, pycalc.py_calculator, "1-") - self.assertRaises(ValueError, pycalc.py_calculator, "1 2") - self.assertRaises(ValueError, pycalc.py_calculator, "==7") - self.assertRaises(ValueError, pycalc.py_calculator, "1 + 2(3 * 4))") - self.assertRaises(ValueError, pycalc.py_calculator, "((1+2)") - self.assertRaises(ValueError, pycalc.py_calculator, "1 + 1 2 3 4 5 6 ") - self.assertRaises(ValueError, pycalc.py_calculator, "log100(100)") - self.assertRaises(ValueError, pycalc.py_calculator, "------") - self.assertRaises(ValueError, pycalc.py_calculator, "5 > = 6") - self.assertRaises(ValueError, pycalc.py_calculator, "5 / / 6") - self.assertRaises(ValueError, pycalc.py_calculator, "6 < = 6") - self.assertRaises(ValueError, pycalc.py_calculator, "6 * * 6") - self.assertRaises(ValueError, pycalc.py_calculator, "(((((") - self.assertRaises(ValueError, pycalc.py_calculator, "abs") - self.assertRaises(ValueError, pycalc.py_calculator, "abs+1") - self.assertRaises(ValueError, pycalc.py_calculator, "isclose(1)") - self.assertRaises(ValueError, pycalc.py_calculator, "cos(2,1)") - self.assertRaises(ValueError, pycalc.py_calculator, "2**2") - self.assertRaises(ValueError, pycalc.py_calculator, "pow(2, 3, 4)") - self.assertRaises(ValueError, pycalc.py_calculator, "fsum[1,,2,3]") + self.assertRaises(ValueError, pycalc.calculate, "") + self.assertRaises(ValueError, pycalc.calculate, "+") + self.assertRaises(ValueError, pycalc.calculate, "1-") + self.assertRaises(ValueError, pycalc.calculate, "1 2") + self.assertRaises(ValueError, pycalc.calculate, "==7") + self.assertRaises(ValueError, pycalc.calculate, "1 + 2(3 * 4))") + self.assertRaises(ValueError, pycalc.calculate, "((1+2)") + self.assertRaises(ValueError, pycalc.calculate, "1 + 1 2 3 4 5 6 ") + self.assertRaises(ValueError, pycalc.calculate, "log100(100)") + self.assertRaises(ValueError, pycalc.calculate, "------") + self.assertRaises(ValueError, pycalc.calculate, "5 > = 6") + self.assertRaises(ValueError, pycalc.calculate, "5 / / 6") + self.assertRaises(ValueError, pycalc.calculate, "6 < = 6") + self.assertRaises(ValueError, pycalc.calculate, "6 * * 6") + self.assertRaises(ValueError, pycalc.calculate, "(((((") + self.assertRaises(ValueError, pycalc.calculate, "abs") + self.assertRaises(ValueError, pycalc.calculate, "abs+1") + self.assertRaises(ValueError, pycalc.calculate, "isclose(1)") + self.assertRaises(ValueError, pycalc.calculate, "cos(2,1)") + self.assertRaises(ValueError, pycalc.calculate, "2**2") + self.assertRaises(ValueError, pycalc.calculate, "pow(2, 3, 4)") + self.assertRaises(ValueError, pycalc.calculate, "fsum[1,,2,3]") if __name__ == '__main__': From f14ce69b1f2b9594ca3974e658aabea3366a3968 Mon Sep 17 00:00:00 2001 From: Daniil Date: Mon, 20 May 2019 19:35:33 +0300 Subject: [PATCH 10/11] added coments --- final_task/pycalc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index 4bde0a71..92f021cd 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -368,8 +368,10 @@ def calculate(math_expression: str) -> Union[int, float, bool, tuple]: op1 = stack.pop() if stack[-1] == ',': raise ValueError(f'invalid number of arguments') + # ldexp is a function from the math module elif token == 'ldexp': stack.append(FUNCTIONS[token](op1, int(op2))) + # gcd is a function from the math module elif token == 'gcd': stack.append(FUNCTIONS[token](int(op1), int(op2))) else: From 49f0428050b68cbc4615b3fb858e46ee5d05e34a Mon Sep 17 00:00:00 2001 From: Daniil Date: Wed, 22 May 2019 20:08:26 +0300 Subject: [PATCH 11/11] Divided big function into small ones. --- final_task/pycalc.py | 216 ++++++++++++++++++++++++------------------- 1 file changed, 121 insertions(+), 95 deletions(-) diff --git a/final_task/pycalc.py b/final_task/pycalc.py index 92f021cd..be94a856 100644 --- a/final_task/pycalc.py +++ b/final_task/pycalc.py @@ -241,6 +241,62 @@ def transform_to_polish_notation(parse_expression: List[elements_of_expression]) stack = [] reverse_polish_notation = '' separator = ' ' + + def check_stack(element): + """checks what is on top of the stack and add operator to stack depending on operator priority + + :param element: operator + + """ + nonlocal reverse_polish_notation + if stack: + if stack[-1] == '(': + stack.append(element) + elif OPERATORS[element][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + stack.append(element) + elif OPERATORS[element][-1] > OPERATORS[stack[-1]][-1]: + stack.append(element) + elif not stack: + stack.append(element) + + def add_operators(element): + nonlocal reverse_polish_notation + if not stack: + stack.append(element) + elif element == stack[-1] and element == '^': + stack.append(element) + elif stack[-1] == '(': + stack.append(element) + elif stack[-1] in FUNCTIONS or stack[-1] in BUILT_IN_FUNCTION: + reverse_polish_notation += stack.pop() + separator + check_stack(element) + elif element == '-' and parse_expression[parse_expression.index(element) - 1] in '/*^%//': + if OPERATORS[element][-1] <= OPERATORS[stack[-1]][-1]: + stack.append(element) + reverse_polish_notation += '0' + separator + elif OPERATORS[element][-1] <= OPERATORS[stack[-1]][-1]: + reverse_polish_notation += stack.pop() + separator + check_stack(element) + else: + stack.append(element) + + def add_tokens_between_brackets(): + nonlocal reverse_polish_notation + for element in stack[::-1]: + if element == '(': + break + reverse_polish_notation += stack.pop() + separator + stack.pop() + + def add_tokens_between_bracket_and_comma(): + nonlocal reverse_polish_notation + for element in stack[::-1]: + if element == '(': + break + reverse_polish_notation += stack.pop() + separator + reverse_polish_notation += token + separator + for token in parse_expression: if re.fullmatch(ALL_NUMBERS, token) and float(token) >= 0: reverse_polish_notation += token + separator @@ -248,56 +304,15 @@ def transform_to_polish_notation(parse_expression: List[elements_of_expression]) # writes a negative number of -5 in the form of 0 5 -, according to the rules of the reverse notation reverse_polish_notation += '0' + separator + token[1:] + separator + token[0] + separator elif token == ')': - for element in stack[::-1]: - if element == '(': - break - reverse_polish_notation += stack.pop() + separator - stack.pop() + add_tokens_between_brackets() elif token == ',': - for element in stack[::-1]: - if element == '(': - break - reverse_polish_notation += stack.pop() + separator - reverse_polish_notation += token + separator + add_tokens_between_bracket_and_comma() elif token in ['(', ':']: stack.append(token) elif token in FUNCTIONS or token in BUILT_IN_FUNCTION: stack.append(token) elif token in OPERATORS: - if not stack: - stack.append(token) - elif token == stack[-1] and token == '^': - stack.append(token) - elif stack[-1] == '(': - stack.append(token) - elif stack[-1] in FUNCTIONS or stack[-1] in BUILT_IN_FUNCTION: - reverse_polish_notation += stack.pop() + separator - if not stack: - stack.append(token) - elif stack[-1] in OPERATORS: - if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - reverse_polish_notation += stack.pop() + separator - stack.append(token) - elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: - stack.append(token) - elif token == '-' and parse_expression[parse_expression.index(token) - 1] in '/*^%//': - if OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - stack.append(token) - reverse_polish_notation += '0' + separator - elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - reverse_polish_notation += stack.pop() + separator - if stack: - if stack[-1] == '(': - stack.append(token) - elif OPERATORS[token][-1] <= OPERATORS[stack[-1]][-1]: - reverse_polish_notation += stack.pop() + separator - stack.append(token) - elif OPERATORS[token][-1] > OPERATORS[stack[-1]][-1]: - stack.append(token) - elif not stack: - stack.append(token) - else: - stack.append(token) + add_operators(token) elif token in CONSTANTS: reverse_polish_notation += token + separator elif token in ('True', 'False'): @@ -322,63 +337,74 @@ def quantity_of_arguments(function: str) -> int: def calculate(math_expression: str) -> Union[int, float, bool, tuple]: reverse_polish_notation = transform_to_polish_notation(exec_isclose(check_expression(parse(math_expression)))) stack = [0] + + def calculate_built_in_function(element: str): + if stack[-2] == ',': + op2 = stack.pop() + stack.pop() + op1 = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + else: + stack.append(BUILT_IN_FUNCTION[element](op1, int(op2))) + else: + op = stack.pop() + stack.append(BUILT_IN_FUNCTION[element](op)) + + def calculate_function_with_two_arg(element: str): + if stack[-2] == ',': + op2 = stack.pop() + stack.pop() + op1 = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + # ldexp is a function from the math module + elif element == 'ldexp': + stack.append(FUNCTIONS[element](op1, int(op2))) + # gcd is a function from the math module + elif element == 'gcd': + stack.append(FUNCTIONS[element](int(op1), int(op2))) + else: + stack.append(FUNCTIONS[element](op1, op2)) + else: + op = stack.pop() + stack.append(FUNCTIONS[element](op)) + + def calculate_single_arg_function(element: str): + # fsum is a function from the math module + if element == 'fsum': + numbers = [] + for index, number in enumerate(stack[::-1]): + if isinstance(number, (int, float)) and stack[::-1][1] == ',': + numbers.append(stack.pop()), stack.pop() + elif len(stack) > index + 2: + if isinstance(stack[::-1][index + 2], (int, float)) and stack[::-1][3] == ',': + numbers.append(stack.pop(-3)), stack.pop(), stack.pop(), stack.pop() + if stack[-1] == ':': + numbers.append(stack.pop(-3)), stack.pop(), stack.pop() + else: + numbers.append(stack.pop()) + stack.extend([numbers]) + op = stack.pop() + stack.append(FUNCTIONS[element](op)) + numbers.clear() + else: + op = stack.pop() + if stack[-1] == ',': + raise ValueError(f'invalid number of arguments') + stack.append(FUNCTIONS[element](op)) + for token in reverse_polish_notation.split(' '): if token in OPERATORS: - op2, op1 = stack.pop(), stack.pop() - stack.append(OPERATORS[token][0](op1, op2)) + oper2, oper1 = stack.pop(), stack.pop() + stack.append(OPERATORS[token][0](oper1, oper2)) elif token in BUILT_IN_FUNCTION: - if stack[-2] == ',': - op2 = stack.pop() - stack.pop() - op1 = stack.pop() - if stack[-1] == ',': - raise ValueError(f'invalid number of arguments') - else: - stack.append(BUILT_IN_FUNCTION[token](op1, int(op2))) - else: - op = stack.pop() - stack.append(BUILT_IN_FUNCTION[token](op)) + calculate_built_in_function(token) elif token in FUNCTIONS: if quantity_of_arguments(FUNCTIONS[token]) == 1: - if token == 'fsum': - numbers = [] - for index, number in enumerate(stack[::-1]): - if isinstance(number, (int, float)) and stack[::-1][1] == ',': - numbers.append(stack.pop()), stack.pop() - elif len(stack) > index + 2: - if isinstance(stack[::-1][index + 2], (int, float)) and stack[::-1][3] == ',': - numbers.append(stack.pop(-3)), stack.pop(), stack.pop(), stack.pop() - if stack[-1] == ':': - numbers.append(stack.pop(-3)), stack.pop(), stack.pop() - else: - numbers.append(stack.pop()) - stack.extend([numbers]) - op = stack.pop() - stack.append(FUNCTIONS[token](op)) - numbers.clear() - else: - op = stack.pop() - if stack[-1] == ',': - raise ValueError(f'invalid number of arguments') - stack.append(FUNCTIONS[token](op)) + calculate_single_arg_function(token) elif quantity_of_arguments(FUNCTIONS[token]) == 2: - if stack[-2] == ',': - op2 = stack.pop() - stack.pop() - op1 = stack.pop() - if stack[-1] == ',': - raise ValueError(f'invalid number of arguments') - # ldexp is a function from the math module - elif token == 'ldexp': - stack.append(FUNCTIONS[token](op1, int(op2))) - # gcd is a function from the math module - elif token == 'gcd': - stack.append(FUNCTIONS[token](int(op1), int(op2))) - else: - stack.append(FUNCTIONS[token](op1, op2)) - else: - op = stack.pop() - stack.append(FUNCTIONS[token](op)) + calculate_function_with_two_arg(token) elif token in [',', ':']: stack.append(token) elif token in CONSTANTS: