diff --git a/final_task/__init__.py b/final_task/pycalc/__init__.py similarity index 100% rename from final_task/__init__.py rename to final_task/pycalc/__init__.py diff --git a/final_task/pycalc/pycalc.py b/final_task/pycalc/pycalc.py new file mode 100644 index 0000000..65ea508 --- /dev/null +++ b/final_task/pycalc/pycalc.py @@ -0,0 +1,513 @@ +import math +import argparse +import numbers + +constants = {a: getattr(math, a) for a in dir(math) if isinstance(getattr(math, a), numbers.Number)} + +comparison = { + "<": lambda x, y: x < y, + ">": lambda x, y: x > y, + ">=": lambda x, y: x >= y, + "<=": lambda x, y: x <= y, + "!=": lambda x, y: x != y, + "==": lambda x, y: x == y +} + +signs = { + "+": lambda x, y: x + y, + "-": lambda x, y: x - y, + "*": lambda x, y: x * y, + "/": lambda x, y: x / y, + "%": lambda x, y: x % y, + "//": lambda x, y: x // y, + "^": lambda x, y: x ** y +} + +functions = {a: getattr(math, a) for a in dir(math) if callable(getattr(math, a))} +functions['abs'] = abs +functions['round'] = round + +priority = { + "+": 0, + "-": 0, + "*": 1, + "/": 1, + "%": 1, + "//": 1, + "^": 2 +} + + +class BracketsNotBalancedException(Exception): + pass + + +class UnknownFunctionException(Exception): + pass + + +class UnknownElementException(Exception): + pass + + +class UnexpectedSpaceExeption(Exception): + pass + + +class MissingArgumentException(Exception): + pass + + +class FunctionArgumentsException(Exception): + pass + + +class EmptyBracketsException(Exception): + pass + + +def is_float(string): + """Check is string float""" + try: + float(string) + return True + except ValueError: + return False + + +def replace_long_unaries(expression): + """Replaces '-' and '+' signs to one '+' or '-' sign, if there are more then one of them""" + number_of_minuses = 0 + number_of_symbols = 0 + start = -1 + is_unar = False + end = -1 + result_expression = expression + for i in range(len(result_expression)): + if result_expression[i] == "+" or result_expression[i] == "-": + if not is_unar: + start = i + is_unar = True + if result_expression[i] == "-": + number_of_minuses += 1 + number_of_symbols += 1 + elif start != -1 and number_of_symbols > 1: + end = i + if number_of_minuses % 2: + result_expression = result_expression.replace( + result_expression[start:end], "-") + result_expression = replace_long_unaries(result_expression) + break + else: + result_expression = result_expression.replace( + result_expression[start:end], "+") + result_expression = replace_long_unaries(result_expression) + break + elif number_of_symbols == 1: + start = -1 + is_unar = False + number_of_minuses = 0 + number_of_symbols = 0 + if start != -1 and end == -1: + raise MissingArgumentException( + "Not enough argumets for binary operation") + return result_expression + + +def checking_and_solving_comparison(expression): + """ + Checks the expression for the presence of comparison and solves it if it is + Parameters + ---------- + expression : str + The expression that is searched for comparison + Returns + ------- + str + Resolved comparison if there was a comparison or expression unchanged if there no comparison + boolean + Is the expression comparison + """ + is_comparison = False + for i in range(len(expression)): + if comparison.get(expression[i]) is not None: + is_comparison = True + if comparison.get(expression[i]+expression[i+1]) is not None: + return [comparison[expression[i]+expression[i+1]](calc(expression[0:i]), + calc(expression[i+2:len(expression)])), is_comparison] + else: + return [comparison[expression[i]](calc(expression[0:i]), + calc(expression[i+1:len(expression)])), is_comparison] + elif i+1 < len(expression) and comparison.get(expression[i]+expression[i+1]) is not None: + is_comparison = True + return [comparison[expression[i]+expression[i+1]](calc(expression[0:i]), + calc(expression[i+2:len(expression)])), is_comparison] + return [expression, is_comparison] + + +def find_left_element(expression, pointer): + """ + Find nearest element from the left to a pointer + Parameters + ---------- + expression : str + The expression that is searched for element + pointer : int + Position to start searching + Returns + ------- + str + Nearest element from the left to a pointer + int + Start position of element + """ + first_element = "" + start = 0 + for i in range(1, pointer+1): + prev = first_element + first_element = expression[pointer-i]+first_element + if not is_float(first_element): + first_element = prev + start = pointer-i+1 + break + elif expression[pointer-i] == '+' or expression[pointer-i] == '-': + if pointer-i-1 >= 0 and (expression[pointer-i-1].isdigit() or expression[pointer-i-1] == ")"): + first_element = prev + start = pointer-i+1 + break + else: + start = pointer-i + break + return [first_element, start] + + +def find_right_element(expression, pointer): + """ + Find nearest element from the right to a pointer + Parameters + ---------- + expression : str + The expression that is searched for element + pointer : int + Position to start searching + Returns + ------- + str + Nearest element from the right to a pointer + int + End position of element + """ + end = 0 + flag = False + second_number = "" + for i in range(pointer+1, len(expression)): + prev = second_number + second_number += expression[i] + if second_number == '-': + continue + elif not is_float(second_number): + flag = True + second_number = prev + end = i-1 + break + if not flag: + end = i + return [second_number, end] + + +def calc_by_position_of_sign(position, expression): + """ + Calculates two nearest elements (from the left and right) according to a sign at 'position' + Parameters + ---------- + expression : str + The expression that is calculated + position : int + Position of sign in expression + Returns + ------- + float + Result of calculation + int + Start position of left element + int + End of right element + """ + if position == 0: + raise MissingArgumentException( + "Not enough argumets for binary operation") + right_pointer = position + left_pointer = position + if position+1 == len(expression): + raise MissingArgumentException( + "Not enough argumets for binary operation") + if signs.get(expression[position]+expression[position+1]) is not None: + right_pointer = position+1 + elif signs.get(expression[position]+expression[position-1]) is not None: + left_pointer = position-1 + [first_number, start] = find_left_element(expression, left_pointer) + [second_number, end] = find_right_element(expression, right_pointer) + if first_number == "" or second_number == "": + raise MissingArgumentException( + "Not enough argumets for binary operation") + if left_pointer == right_pointer: + return [signs[expression[position]](float(first_number), float(second_number)), start, end] + else: + return [signs["//"](float(first_number), float(second_number)), start, end] + + +def calc_string(expression): + """ + Calculates expression, consisting of float numbers and signs of operations + Parameters + ---------- + expression : str + The expression that is calculated + Returns + ------- + float + Result of calculation + """ + if is_float(expression): + return float(expression) + maxprior = -1 + position = 0 + for i in range(0, len(expression)): + if (expression[i] == '-' or expression[i] == '+') and i == 0: + continue + if expression[i] in ('+', '-', '*', '/', '^', '%') and priority[expression[i]] > maxprior: + position = i + maxprior = priority[expression[i]] + elif expression[i] in ('+', '-', '*', '/', '^', '%') and maxprior == 2 and priority[expression[i]] >= maxprior: + position = i + maxprior = priority[expression[i]] + result = calc_by_position_of_sign(position, expression) + new_string = expression.replace( + expression[result[1]:result[2]+1], str("{:.16f}".format(result[0]))) + return calc_string(new_string) + + +def find_and_replace_consts(expression): + """Replaces constatnts in the 'expression'""" + if is_float(expression): + return expression + temp_expression = expression + for i in constants: + temp_expression = temp_expression.replace(i, str(constants[i])) + return temp_expression + + +def add_implicit_mult(expression): + """Adds multiplication sign where it is given implicitly""" + result_expression = expression + expr_left = "" + expr_right = "" + was_float = False + was_const = False + for i in range(len(result_expression)): + expr_right += result_expression[i] + if result_expression[i] in ("+", "-", "*", "^", "%", "=", ">", "<", "!", "/", "(", ")", ","): + if result_expression[i] == ")" and i+1 < len(result_expression): + if not result_expression[i+1] in ("+", "-", "*", "%", "^", "=", ">", "<", "!", "/", ")", ","): + result_expression = result_expression[0:i+1] + \ + "*"+result_expression[i+1:len(result_expression)] + expr_left = expr_right + expr_right = "" + was_const = False + was_float = False + if result_expression[i] == "(" and is_float(expr_left[0:len(expr_left)-1]): + result_expression = result_expression[0:i] + \ + "*"+result_expression[i:len(result_expression)] + elif is_float(expr_right): + was_float = True + elif not is_float(expr_right) and was_float: + result_expression = result_expression[0:i] + \ + "*"+result_expression[i:len(result_expression)] + was_float = False + elif constants.get(expr_right) is not None: + was_const = True + elif constants.get(expr_right) is None and was_const: + is_func = False + temp = expr_right + for j in range(i+1, len(result_expression)): + if functions.get(temp) is not None: + is_func = True + break + temp += result_expression[j] + if not is_func: + result_expression = result_expression[0:i] + \ + "*"+result_expression[i:len(result_expression)] + was_const = False + return result_expression + + +def solve_brackets(expression): + """Repalces expression in brackets on it's value""" + result_string = expression + start = -1 + brackets_balance = 0 + for i in range(len(expression)): + if expression[i] == '(': + if brackets_balance == 0: + start = i + brackets_balance += 1 + elif expression[i] == ')': + brackets_balance -= 1 + if start != -1 and brackets_balance == 0: + end = i + if start+1 == end: + raise EmptyBracketsException("Empty brackets") + result_string = result_string.replace( + result_string[start:end+1], str("{:.16f}".format(calc(result_string[start+1:end])))) + result_string = solve_brackets(result_string) + break + if brackets_balance != 0: + raise BracketsNotBalancedException("brackets not balanced") + return result_string + + +def solve_functions(expression): + """Findes and replaces functions to it's value. Solves expression in arguments, if it is necessary""" + res_str = expression + is_func = False + brackets_balance = 0 + temp = "" + end = 0 + first_end = end + for i in range(len(expression)): + if not (res_str[i].isdigit() or res_str[i] in (".", '+', '-', '*', '/', '^', '%', ')', '(')): + if not is_func: + func_start = i + is_func = True + temp += res_str[i] + elif not res_str[i] in (".", '+', '-', '*', '/', '^', '%', ')', '(') and is_func: + temp += res_str[i] + elif res_str[i] == '(' and is_func: + if functions.get(temp) is not None: + start = i+1 + for j in range(i, len(expression)): + if expression[j] == '(': + brackets_balance += 1 + elif expression[j] == ',' and brackets_balance == 1: + first_end = j + elif expression[j] == ')': + brackets_balance -= 1 + if brackets_balance == 0: + end = j + break + if first_end: + try: + res_str = res_str.replace( + res_str[func_start:end+1], + str("{:.16f}".format(functions[temp](calc(res_str[start:first_end]), + calc(res_str[first_end+1:end]))))) + res_str = solve_functions(res_str) + break + except Exception: + raise FunctionArgumentsException(f"Incorrect arguments in function '{temp}'") + else: + try: + res_str = res_str.replace(res_str[func_start:end+1], + str("{:.16f}".format(functions[temp](calc(res_str[start:end]))))) + res_str = solve_functions(res_str) + break + except Exception: + raise FunctionArgumentsException(f"Incorrect arguments in function '{temp}'") + else: + raise UnknownFunctionException(f"Unknown function '{temp}'") + elif res_str[i] in (".", '+', '-', '*', '/', '^', '%'): + if temp != "" and functions.get(temp) is None and constants.get(temp) is None: + raise UnknownElementException(f"Unknown element '{temp}'") + temp = "" + is_func = False + if temp != "" and functions.get(temp) is None and constants.get(temp) is None: + raise UnknownElementException(f"Unknown element '{temp}'") + return res_str + + +def replace_spaces(expression): + """Findes and removes unenecessary spaces near signs""" + res_exp = expression + space_pos = res_exp.find(" ") + while space_pos != -1: + if space_pos-1 >= 0 and res_exp[space_pos-1] in ("+", "-", "*", "^", "%", "=", ">", "<", "!", "/", ","): + if space_pos+1 < len(res_exp) and res_exp[space_pos+1] in ("*", "^", "=", ">", "<", "!", "/", "%"): + error = f"Unexpected space between '{res_exp[space_pos-1]}' and '{res_exp[space_pos+1]}'" + raise UnexpectedSpaceExeption(error) + else: + res_exp = res_exp.replace( + res_exp[space_pos], "", 1) + elif space_pos+1 < len(res_exp) and res_exp[space_pos+1] in ("+", "-", "*", "^", "%", "=", ">", "<", "!", "/"): + if space_pos-1 >= 0 and res_exp[space_pos-1] in ("+", "-", "*", "^", "%", "=", ">", "<", "!", "/"): + error = f"Unexpected space between '{res_exp[space_pos-1]}' and '{res_exp[space_pos+1]}'" + raise UnexpectedSpaceExeption(error) + else: + res_exp = res_exp.replace( + res_exp[space_pos], "", 1) + elif space_pos-1 >= 0 and res_exp[space_pos-1] == ')': + if space_pos+1 < len(res_exp): + res_exp = res_exp.replace( + res_exp[space_pos], "", 1) + else: + raise UnexpectedSpaceExeption("Unexpected space in the end of expression") + else: + elem = "" + if space_pos-1 >= 0: + for i in range(1, space_pos+1): + prev = elem + elem = res_exp[space_pos-i]+elem + if res_exp[space_pos-i] in ("+", "-", "*", "^", "%", "=", ">", "<", "!", "/", ",", "(", ")", " "): + elem = prev + break + if constants.get(elem) is not None: + res_exp = res_exp.replace( + res_exp[space_pos], "", 1) + space_pos = res_exp.find(" ") + continue + elif is_float(elem): + if space_pos+1 < len(res_exp) and not is_float(res_exp[space_pos+1]): + res_exp = res_exp.replace( + res_exp[space_pos], "", 1) + space_pos = res_exp.find(" ") + continue + raise UnexpectedSpaceExeption("Unexpected space") + space_pos = res_exp.find(" ") + return res_exp + + +def calc(expression): + """Calculate expression with no spaces""" + result_expression = expression + result_expression = replace_long_unaries(result_expression) + result_expression = solve_functions(result_expression) + result_expression = replace_long_unaries(result_expression) + result_expression = solve_brackets(result_expression) + result_expression = replace_long_unaries(result_expression) + result_expression = find_and_replace_consts(result_expression) + result_expression = replace_long_unaries(result_expression) + return calc_string(result_expression) + + +def main(): + """Main function, that parse arguments and gives the result of calculation""" + parser = argparse.ArgumentParser( + description='Pure-python command-line calculator.') + parser.add_argument('EXPRESSION', help='expression string to evaluate') + string = parser.parse_args() + try: + expression = string.EXPRESSION + expression = replace_spaces(expression) + expression = add_implicit_mult(expression) + [expression, is_comparison] = checking_and_solving_comparison( + expression) + if is_comparison: + print(expression) + else: + print(calc(expression)) + except Exception as e: + print(f"ERROR: {e}") + return e + + +if __name__ == "__main__": + main() diff --git a/final_task/pycalc/testPycalc.py b/final_task/pycalc/testPycalc.py new file mode 100644 index 0000000..e2a77f5 --- /dev/null +++ b/final_task/pycalc/testPycalc.py @@ -0,0 +1,195 @@ +import unittest +import math +from pycalc.pycalc import ( + find_right_element, + find_left_element, + replace_long_unaries, + checking_and_solving_comparison, + calc_by_position_of_sign, + calc_string, + find_and_replace_consts, + add_implicit_mult, + solve_brackets, + solve_functions, + replace_spaces, + calc, + MissingArgumentException, + BracketsNotBalancedException, + UnknownFunctionException, + UnknownElementException, + UnexpectedSpaceExeption, + FunctionArgumentsException, + EmptyBracketsException +) + + +class TestPycalc(unittest.TestCase): + + def test_find_left_number_near_the_sign(self): + self.assertEqual(find_left_element("2+2", 1), ["2", 0]) + + def test_find_left_neg_number_near_the_sign_test(self): + self.assertEqual(find_left_element("4*-3+3", 4), ["-3", 2]) + + def test_find_right_number_near_the_sign(self): + self.assertEqual(find_right_element("2+2", 1), ["2", 2]) + + def test_find_right_neg_number_near_the_sign(self): + self.assertEqual(find_right_element("2*2+3*-4", 5), ["-4", 7]) + + def test_replaces_long_unaries_before_number(self): + self.assertEqual(replace_long_unaries("---+-+-3"), "-3") + + def test_replaces_long_unaries_in_middle_of_expression(self): + self.assertEqual(replace_long_unaries("3--+-+--+-4"), "3+4") + + def test_replaces_long_unaries_in_the_end_of_expression(self): + with self.assertRaises(MissingArgumentException): + replace_long_unaries("3--+-+--+-") + + def test_find_and_replace_consts_with_e_const(self): + self.assertEqual(find_and_replace_consts("e"), str(math.e)) + + def test_find_and_replace_consts_in_expression(self): + result = "1+4*"+str(math.e)+"+"+str(math.pi) + self.assertEqual(find_and_replace_consts("1+4*e+pi"), result) + + def test_find_and_replace_consts_with_no_consts(self): + self.assertEqual(find_and_replace_consts("e"), str(math.e)) + + def test_checking_and_solving_comparison_on_comparison_without_solving(self): + self.assertEqual(checking_and_solving_comparison("3<2"), [False, True]) + + def test_checking_and_solving_comparison_on_comparison_with_solving(self): + self.assertEqual(checking_and_solving_comparison("3+5<2+10"), [True, True]) + + def test_checking_and_solving_comparison_on_expression_without_comparison(self): + self.assertEqual(checking_and_solving_comparison("3+5"), ["3+5", False]) + + def test_calc_by_position_of_sign(self): + self.assertEqual(calc_by_position_of_sign(5, "3+4+5*7"), [35.0, 4, 6]) + + def test_calc_by_position_of_sign_with_neg_left_number(self): + self.assertEqual(calc_by_position_of_sign(2, "-5*7"), [(float)(-5*7), 0, 3]) + + def test_calc_by_position_of_sign_with_zero_pointer(self): + with self.assertRaises(MissingArgumentException): + calc_by_position_of_sign(0, "-5*7") + + def test_calc_by_position_of_sign_with_no_right_arg_because_of_end_of_expr(self): + with self.assertRaises(MissingArgumentException): + calc_by_position_of_sign(1, "7*") + + def test_calc_by_position_of_sign_with_no_right_arg(self): + with self.assertRaises(MissingArgumentException): + calc_by_position_of_sign(1, "7*/3") + + def test_calc_string_with_unaries(self): + self.assertEqual(calc_string("-5*7"), (float)(-5*7)) + + def test_calc_string_with_three_signs(self): + self.assertEqual(calc_string("-5*7*4^2"), (float)(-5*7*4**2)) + + def test_calc_string_check_associative(self): + self.assertEqual(calc_string("2+2*2"), (float)(2+2*2)) + + def test_calc_string_check_associative_on_powers(self): + self.assertEqual(calc_string("2^2^2"), (float)(2**2**2)) + + def test_add_implicit_multiplication_sign_after_brackets(self): + self.assertEqual(add_implicit_mult("(3+4)2"), "(3+4)*2") + + def test_add_implicit_multiplication_sign_before_brackets(self): + self.assertEqual(add_implicit_mult("2(3+4)"), "2*(3+4)") + + def test_add_implicit_multiplication_sign_between_brackets(self): + self.assertEqual(add_implicit_mult("(3+4)(2+3)"), "(3+4)*(2+3)") + + def test_add_implicit_multiplication_sign_before_func(self): + self.assertEqual(add_implicit_mult("3sin(3)"), "3*sin(3)") + + def test_add_implicit_multiplication_sign_before_const(self): + self.assertEqual(add_implicit_mult("3e"), "3*e") + + def test_add_implicit_multiplication_sign_after_func(self): + self.assertEqual(add_implicit_mult("sin(3)3"), "sin(3)*3") + + def test_add_implicit_multiplication_sign_after_const(self): + self.assertEqual(add_implicit_mult("e3"), "e*3") + + def test_add_implicit_multiplication_sign_between_consts(self): + self.assertEqual(add_implicit_mult("pie"), "pi*e") + + def test_solve_brackets_general(self): + self.assertEqual(solve_brackets("(2+3)*3"), "5.0000000000000000*3") + + def test_solve_brackets_with_unbalanced_brackets(self): + with self.assertRaises(BracketsNotBalancedException): + solve_brackets("2+3(") + + def test_solve_brackets_with_empty_brackets(self): + with self.assertRaises(EmptyBracketsException): + solve_brackets("2+3()") + + def test_solve_functions_general(self): + self.assertEqual(solve_functions("sin(3)"), str(math.sin(3))) + + def test_solve_functions_with_two_elems(self): + self.assertEqual(solve_functions("sin(3)+cos(3)"), str(math.sin(3))+'+'+str(math.cos(3))) + + def test_solve_functions_with_two_args(self): + self.assertEqual(solve_functions("log(120,10)"), str(math.log(120, 10))) + + def test_solve_functions_with_solving_in_args(self): + self.assertEqual(solve_functions("sin(3+5)"), str(math.sin(3+5))) + + def test_solve_functions_with_wrong_number_of_arguments(self): + with self.assertRaises(FunctionArgumentsException): + solve_functions("log(3,4,5)") + + def test_solve_functions_with_wrong_function_name(self): + with self.assertRaises(UnknownFunctionException): + solve_functions("logs(3)") + + def test_solve_functions_with_wrong_element_name(self): + with self.assertRaises(UnknownElementException): + solve_functions("3+logs+3") + + def test_replace_spaces_in_func(self): + self.assertEqual(replace_spaces("sin(3 + 5)"), "sin(3+5)") + + def test_replace_spaces_in_func_with_two_args(self): + self.assertEqual(replace_spaces("log(10, 5)"), "log(10,5)") + + def test_replace_spaces_err_in_comparison_sign(self): + with self.assertRaises(UnexpectedSpaceExeption): + replace_spaces("3< =4") + + def test_replace_spaces_err_func(self): + with self.assertRaises(UnexpectedSpaceExeption): + replace_spaces("sin (3+4)") + + def test_replace_spaces_for_implicit_mult_number_before_func(self): + self.assertEqual(replace_spaces("2 sin(3)"), "2sin(3)") + + def test_replace_spaces_for_implicit_mult_number_after_func(self): + self.assertEqual(replace_spaces("sin(3) 2"), "sin(3)2") + + def test_replace_spaces_for_implicit_mult_between_brackets(self): + self.assertEqual(replace_spaces("(2+3) (3+4)"), "(2+3)(3+4)") + + def test_replace_spaces_for_implicit_mult_with_const_after_number(self): + self.assertEqual(replace_spaces("2 e"), "2e") + + def test_replace_spaces_for_implicit_mult_with_const_before_number(self): + self.assertEqual(replace_spaces("pi 2"), "pi2") + + def test_calc_general(self): + self.assertEqual(calc("3+log(e)+4/2^2"), 3+math.log(math.e)+4/2**2) + + def test_calc_with_unaries(self): + self.assertEqual(calc("3+log(e)+--+-4/2^2"), 3+math.log(math.e)+--+-4/2**2) + + +if __name__ == "__main__": + unittest.main() diff --git a/final_task/setup.py b/final_task/setup.py index e69de29..d4b4df9 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup, find_packages + +setup( + name="Calculator", + author="Andrey Mirugin", + version="1.0", + author_email="andrey.mirugin@gmail.com", + description=("Pure-python command-line calculator."), + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'pycalc = pycalc.pycalc:main', + ] + } +)