diff --git a/final_task/pycalc/__init__.py b/final_task/pycalc/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/pycalc/calculator.py b/final_task/pycalc/calculator.py new file mode 100644 index 00000000..bcb52472 --- /dev/null +++ b/final_task/pycalc/calculator.py @@ -0,0 +1,18 @@ +from . import corrector +from . import converter +from . import evaluator + + +class Calculator(object): + def __init__(self, **kwargs): + + self.corrector = corrector.Corrector() + self.converter = converter.Converter() + self.evaluator = evaluator.Evaluator() + + def calculate(self, iExpr): + """function for final calculate""" + correctedExpr = self.corrector.correct(iExpr) + convertedExpr = self.converter.convert(correctedExpr) + evaluatedExpr = self.evaluator.evaluate(convertedExpr) + return evaluatedExpr diff --git a/final_task/pycalc/calculator_test.py b/final_task/pycalc/calculator_test.py new file mode 100644 index 00000000..3f9240ba --- /dev/null +++ b/final_task/pycalc/calculator_test.py @@ -0,0 +1,150 @@ +import unittest +from . import calculator +import math + + +class TestCalculate(unittest.TestCase): + def setUp(self): + self.calculator = calculator.Calculator() + + def testUnary(self): + iExpr = "-13" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = "6-(-13)" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = "1---1" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = "-+---+-1" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + + def testOppriority(self): + iExpr = "1+2*2" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = "1+(2+3*2)*3" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = "10*(2+1)" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = "10^(2+1)" + self.assertEqual(self.calculator.calculate(iExpr), eval("10**(2+1)")) + iExpr = "100/3^2" + self.assertEqual(self.calculator.calculate(iExpr), eval("100/3**2")) + iExpr = "100/3%2^2" + self.assertEqual(self.calculator.calculate(iExpr), eval("100/3%2**2")) + + def testFunc(self): + iExpr = "pi+e" + self.assertEqual(self.calculator.calculate(iExpr), eval("math.pi+math.e")) + iExpr = "log(e)" + self.assertEqual(self.calculator.calculate(iExpr), eval("math.log(math.e)")) + iExpr = "sin(pi/2)" + self.assertEqual(self.calculator.calculate(iExpr), eval("math.sin(math.pi/2)")) + iExpr = "log10(100)" + self.assertEqual(self.calculator.calculate(iExpr), eval("math.log10(100)")) + iExpr = "sin(pi/2)*111*6" + self.assertEqual( + self.calculator.calculate(iExpr), eval("math.sin(math.pi/2)*111*6") + ) + iExpr = "2*sin(pi/2)" + self.assertEqual( + self.calculator.calculate(iExpr), eval("2*math.sin(math.pi/2)") + ) + iExpr = "2^3^2^2" + self.assertEqual(self.calculator.calculate(iExpr), eval("2**3**2**2")) + + def testAssociative(self): + iExpr = r"102%12%7" + self.assertEqual(self.calculator.calculate(iExpr), eval(r"102%12%7")) + iExpr = "100/4/3" + self.assertEqual(self.calculator.calculate(iExpr), eval("100/4/3")) + iExpr = "2^3^4" + self.assertEqual(self.calculator.calculate(iExpr), eval("2**3**4")) + + def testComparison(self): + iExpr = r"1+2*3==1+2*3" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = r"e^5>=e^5+1" + self.assertEqual( + self.calculator.calculate(iExpr), eval("math.e**5>=math.e**5+1") + ) + iExpr = r"1+2*4/3+1!=1+2*4/3+2" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + + def testCommon(self): + iExpr = r"(100)" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = r"666" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = r"-.1" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = r"1/3" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = r"1.0/3.0" + self.assertEqual(self.calculator.calculate(iExpr), eval(iExpr)) + iExpr = r".1 * 2.0^56.0" + self.assertEqual(self.calculator.calculate(iExpr), eval(".1 * 2.0**56.0")) + iExpr = r"e^34" + self.assertEqual(self.calculator.calculate(iExpr), eval("math.e**34")) + iExpr = r"(2.0^(pi/pi+e/e+2.0^0.0))" + self.assertEqual( + self.calculator.calculate(iExpr), + eval("(2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))"), + ) + iExpr = r"(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)" + self.assertEqual( + self.calculator.calculate(iExpr), + eval("(2.0**(math.pi/math.pi+math.e/math.e+2.0**0.0))**(1.0/3.0)"), + ) + iExpr = r"sin(pi/2^1) + log(1*4+2^2+1, 3^2)" + self.assertEqual( + self.calculator.calculate(iExpr), + eval("math.sin(math.pi/2**1) + math.log(1*4+2**2+1, 3**2)"), + ) + iExpr = r"10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5" + self.assertEqual( + self.calculator.calculate(iExpr), + eval("10*math.e**0*math.log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5"), + ) + + iExpr = r"2.0^(2.0^2.0*2.0^2.0)" + self.assertEqual( + self.calculator.calculate(iExpr), eval("2.0**(2.0**2.0*2.0**2.0)") + ) + + """def testError(self): + iExpr = r"((1+2)" + with self.assertRaises(converter.ConvertError): + self.calculator.calculate(iExpr) + iExpr = r"" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + + iExpr = r"(((((" + with self.assertRaises(converter.ConvertError): + self.calculator.calculate(iExpr) + iExpr = r"1 + 1 2 3 4 5 6" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + iExpr = r"1 2" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + iExpr = r"5 > = 6" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + iExpr = r"5 / / 6" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + iExpr = r"6 < = 6" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + iExpr = r"6 * * 6" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) + iExpr = r"log100(100)" + with self.assertRaises(ValueError): + self.calculator.calculate(iExpr) +""" + + +if __name__ == "__main__": + + unittest.main() diff --git a/final_task/pycalc/cli.py b/final_task/pycalc/cli.py new file mode 100644 index 00000000..c74fa7f3 --- /dev/null +++ b/final_task/pycalc/cli.py @@ -0,0 +1,27 @@ +import argparse +from . import calculator + + +def create_parser(): + parser = argparse.ArgumentParser( + description="Pure-python command-line calculator.") + parser.add_argument('EXPRESSION', help='string to evaluate',) + args = parser.parse_args() + iExpr = args.EXPRESSION + return iExpr + + +def main(): + calc = calculator.Calculator() + try: + iExpr = create_parser() + if iExpr: + print(calc.calculate(iExpr)) + else: + raise Exception('empty expression') + except Exception as error: + print("ERROR: " + str(error)) + + +if __name__ == '__main__': + main() diff --git a/final_task/pycalc/converter.py b/final_task/pycalc/converter.py new file mode 100644 index 00000000..9a740241 --- /dev/null +++ b/final_task/pycalc/converter.py @@ -0,0 +1,175 @@ +from . import definitions +from collections import deque +import re + + +class ConvertError(Exception): + """class for error""" + + def __str__(self): + return "cannot convert expression to RPN due to mismatched parentheses" + + +class Converter: + def __init__(self): + self.func = re.compile( + r"(" + + "|".join( + [ + re.escape(func) + for func in sorted( + definitions._functions, key=lambda func: len(func), reverse=True + ) + ] + ) + + ")" + ) + self.num = re.compile(definitions._number) + self.op = re.compile( + r"|".join( + [ + re.escape(op) + for op in sorted( + definitions._operators, key=lambda func: len(func), reverse=True + ) + ] + ) + ) + + def convert(self, iExpr): + """ + function for converting parsed expression in RPN + """ + func = self.func + num = self.num + op = self.op + pos = 0 + operatorStack = deque() + outputStack = deque() + + while pos < len(iExpr): + if num.match(iExpr, pos): + numM = num.match(iExpr, pos) + if "." in numM.group(): + outputStack.append(float(numM.group())) + else: + outputStack.append(int(numM.group())) + pos = numM.end() + elif iExpr[pos] == "(": + operatorStack.appendleft("(") + pos += 1 + elif iExpr[pos] == ")": + if len(operatorStack) == 0: + raise ConvertError() + top = operatorStack.popleft() + while top != "(" and not len(operatorStack) == 0: + outputStack.append(top) + top = operatorStack.popleft() + if top != "(": + raise ConvertError() + pos += 1 + + elif func.match(iExpr, pos): + funcM = func.match(iExpr, pos) + flag = False + try: + a = iExpr[funcM.end() + 1] != "(" + except IndexError: + flag = True + + if not flag and iExpr[funcM.end()] != "(": + raise ValueError("unknown function") + if flag: + raise ValueError("no argument in function") + operatorStack.appendleft(funcM.group()) + pos = funcM.end() + + elif iExpr[pos] == ",": + if operatorStack: + top = operatorStack.popleft() + if (op.match(top) or func.match(top)) and "(" in operatorStack: + while operatorStack: + outputStack.append(top) + top = operatorStack.popleft() + if top == "(": + operatorStack.appendleft(top) + break + elif not op.match(top): + operatorStack.appendleft(top) + else: + raise ConvertError() + outputStack.append(",") + pos += 1 + + elif op.match(iExpr, pos): + match = op.match(iExpr, pos) + if len(operatorStack) != 0: + top = operatorStack.popleft() + + cond = ( + func.match(top) + or ( + op.match(top) + and ( + definitions._operators[top].priority + > definitions._operators[match.group()].priority + ) + ) + or ( + op.match(top) + and ( + definitions._operators[top].priority + == definitions._operators[match.group()].priority + and definitions._operators[top].LAssos + ) + ) + ) and top != "(" + if not cond: + operatorStack.appendleft(top) + operatorStack.appendleft(match.group()) + pos = pos + len(match.group()) + + while cond: + + outputStack.append(top) + if len(operatorStack) != 0: + top = operatorStack.popleft() + cond = ( + func.match(top) + or ( + op.match(top) + and ( + definitions._operators[top].priority + > definitions._operators[match.group()].priority + ) + ) + or ( + op.match(top) + and ( + definitions._operators[top].priority + == definitions._operators[ + match.group() + ].priority + and definitions._operators[top].LAssos + ) + ) + ) and top != "(" + + else: + break + if not cond: + operatorStack.appendleft(top) + break + + else: + + operatorStack.appendleft(match.group()) + pos = pos + len(match.group()) + + while len(operatorStack) != 0: + outputStack.append(operatorStack.popleft()) + # print(outputStack) + if "(" in outputStack or ")" in outputStack: + raise ConvertError() + + return outputStack diff --git a/final_task/pycalc/corrector.py b/final_task/pycalc/corrector.py new file mode 100644 index 00000000..35d43cb1 --- /dev/null +++ b/final_task/pycalc/corrector.py @@ -0,0 +1,208 @@ +import re +import math +from . import definitions + + +class Corrector: + + """base class for correct input string for parsing """ + + def __init__(self): + """declare row(in regex) and compiled part of expression""" + self._expr = "" + self._funcPos = [] + + self.constantR = ( + r"(" + + "|".join([re.escape(const) for const in definitions._constants]) + + ")" + ) + self.functionR = ( + r"(" + + "|".join( + [ + re.escape(func) + for func in sorted( + definitions._functions, key=lambda func: len(func), reverse=True + ) + ] + ) + + ")" + ) + self.operatorR = ( + r"(" + + r"|".join( + [re.escape(op) + for op in definitions._operators if op not in ["+", "-"]] + ) + + r"|=" + r")" + ) + + self.numberR = definitions._number + self.constantC = re.compile(self.constantR) + self.functionC = re.compile(self.functionR) + self.numberC = re.compile(self.numberR) + self.operatorC = re.compile(self.operatorR) + + def findFuncPos(self): + """find functions positions in expression """ + self._funcPos = [] + for result in re.finditer(self.functionC, self._expr): + for pos in range(result.start(), result.end()): + self._funcPos.append(pos) + + def constInterpolation(self): + self.findFuncPos() + strin = self._expr + for result in re.finditer(self.constantC, self._expr): + if result.start() not in self._funcPos: + strin = re.sub( + result.group(), + str(definitions._constants.get(result.group()).value), + strin, + ) + self._expr = strin + + def plusMinusReduce(self): + strin = self._expr + while ( + re.search(re.compile(r"\-\-"), strin) + or re.search(re.compile(r"\+\+"), strin) + or re.search(re.compile(r"\+\-"), strin) + or re.search(re.compile(r"\-\+"), strin) + ): + strin = re.sub(r"\-\-", "+", strin) + strin = re.sub(r"\+\+", "+", strin) + strin = re.sub(r"\+\-", "-", strin) + strin = re.sub(r"\-\+", "-", strin) + self._expr = strin + + def spaceReduce(self): + spaceC = re.compile(self.numberR + r"[\s]+" + self.numberR) + spaceOC = re.compile(self.operatorR + r"[\s]+" + self.operatorR) + strin = self._expr + if re.search(spaceC, strin) or re.search(spaceOC, strin): + raise ValueError("space beatwin operators") + else: + strin = re.sub(r"[\s]+", "", strin) + self._expr = strin + + def unaryPlusReduce(self): + strin = self._expr + operatorMap = {(op + "+"): op for op in definitions._operators} + operatorMapC = re.compile( + r"(" + "|".join([re.escape(op) for op in operatorMap]) + ")" + ) + # print(operatorMap) + if re.search(re.compile(r"^\+"), strin): + strin = re.sub(r"^\+", r"", strin) + if re.search(re.compile(r"\(\+"), strin): + strin = re.sub(r"\(\+", "(", strin) + if re.search(re.compile(r"\,\+"), strin): + strin = re.sub(r"\,\+", ",", strin) + for result in re.finditer(operatorMapC, strin): + strin = re.sub( + re.compile(re.escape(result.group())), + operatorMap.get(result.group()), + strin, + ) + self._expr = strin + + def unaryMinusReduce(self): + minusPredecessors = [] + + minusPredecessors.extend( + [ + re.escape(op) + for op in sorted( + definitions._operators, key=lambda op: len(op), reverse=True + ) + ] + ) + + minusPredecessors.append(r"\A") + + minusPredecessors.append(re.escape("(")) + + unaryMinusBasicR = ( + "(" + "|".join(minusPredecessors) + ")" + + re.escape("-") + self.numberR + ) + unaryMinusBasicM = re.compile(unaryMinusBasicR) + minusPredecessorsM = re.compile("|".join(minusPredecessors)) + + offset = 0 + for result in re.finditer(unaryMinusBasicM, self._expr): + curUnaryMinusPos = 0 + if result.start() > 0: + curUnaryMinusPos = minusPredecessorsM.match( + self._expr, result.start() + offset + ).end() + self._expr = ( + self._expr[:curUnaryMinusPos] + + "(0" + self._expr[curUnaryMinusPos:] + ) + offset += 2 + self._expr = ( + self._expr[: result.end() + offset] + + ")" + + self._expr[result.end() + offset:] + ) + offset += 1 + + minusSuccessors = [] + minusSuccessors.extend( + [ + re.escape(func + "(") + for func in sorted( + definitions._functions, key=lambda func: len(func), reverse=True + ) + ] + ) + minusSuccessors.append(re.escape("(")) + + unaryMinusBracketR = ( + "(" + + "|".join(minusPredecessors) + + ")" + + re.escape("-") + + "(" + + "|".join(minusSuccessors) + + ")" + ) + unaryMinusBracketM = re.compile(unaryMinusBracketR) + + result = unaryMinusBracketM.search(self._expr) + while result: + curUnaryMinusPos = 0 + if result.start() > 0: + curUnaryMinusPos = minusPredecessorsM.match( + self._expr, result.start() + ).end() + self._expr = ( + self._expr[:curUnaryMinusPos] + + "(0" + self._expr[curUnaryMinusPos:] + ) + offset = 2 + + lastBracketPos = result.end() + offset + bracketCounter = 1 + while bracketCounter > 0: + if self._expr[lastBracketPos] == "(": + bracketCounter += 1 + elif self._expr[lastBracketPos] == ")": + bracketCounter -= 1 + lastBracketPos += 1 + self._expr = self._expr[:lastBracketPos] + \ + ")" + self._expr[lastBracketPos:] + result = unaryMinusBracketM.search(self._expr) + + def correct(self, iExpr): + self._expr = iExpr + self.spaceReduce() + self.constInterpolation() + self.plusMinusReduce() + self.unaryPlusReduce() + self.unaryMinusReduce() + return self._expr diff --git a/final_task/pycalc/corrector_test.py b/final_task/pycalc/corrector_test.py new file mode 100644 index 00000000..b15791b4 --- /dev/null +++ b/final_task/pycalc/corrector_test.py @@ -0,0 +1,33 @@ +import unittest +from . import corrector + + +class TestCorrector(unittest.TestCase): + corrector = corrector.Corrector() + + def testPlusMinusReduce(self): + self.corrector._expr = "---5+---2+++8+--4++5*--6+-+-+15-23--(--5)" + self.corrector.plusMinusReduce() + self.assertEqual(self.corrector._expr, + "-5-2+8+4+5*+6+15-23+(+5)") + + def testUnaryPlusReduce(self): + self.corrector._expr = "+8*+3+(+sin())" + self.corrector.unaryPlusReduce() + self.assertEqual(self.corrector._expr, + "8*3+(sin())") + + def testUnaryMinusReduce(self): + self.corrector._expr = "-1*-2-(-3)+3^-2+1" + self.corrector.unaryMinusReduce() + self.assertEqual(self.corrector._expr, + "(0-1)*(0-2)-((0-3))+3^(0-2)+1") + + self.corrector._expr = "-sin(-(1*-(2*3))-(-3))+3^(-2+1)" + self.corrector.unaryMinusReduce() + self.assertEqual(self.corrector._expr, + "(0-sin((0-(1*(0-(2*3))))-((0-3))))+3^((0-2)+1)") + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/pycalc/definitions.py b/final_task/pycalc/definitions.py new file mode 100644 index 00000000..320a6ad4 --- /dev/null +++ b/final_task/pycalc/definitions.py @@ -0,0 +1,36 @@ +import math +from collections import namedtuple + +""" +Definitions for all type of input +""" +Operator = namedtuple("Operator", ["LAssos", "priority", "func"]) +Constant = namedtuple("Constant", ["value"]) +_functions = { + attr: getattr(math, attr) for attr in dir(math) if callable(getattr(math, attr)) +} + +_functions["abs"] = abs +_functions["round"] = round +_number = r"\d*[\.]?\d+" +_operators = { + "^": Operator(LAssos=False, priority=4, func=lambda x, y: x ** y), + "*": Operator(LAssos=True, priority=3, func=lambda x, y: x * y), + "/": Operator(LAssos=True, priority=3, func=lambda x, y: x / y), + "//": Operator(LAssos=True, priority=3, func=lambda x, y: x // y), + "%": Operator(LAssos=True, priority=3, func=lambda x, y: x % y), + "+": Operator(LAssos=True, priority=2, func=lambda x, y: x + y), + "-": Operator(LAssos=True, priority=2, func=lambda x, y: x - y), + "==": Operator(LAssos=True, priority=1, func=lambda x, y: x == y), + "<=": Operator(LAssos=True, priority=1, func=lambda x, y: x <= y), + ">=": Operator(LAssos=True, priority=1, func=lambda x, y: x >= y), + "<": Operator(LAssos=True, priority=1, func=lambda x, y: x < y), + ">": Operator(LAssos=True, priority=1, func=lambda x, y: x > y), + "!=": Operator(LAssos=True, priority=1, func=lambda x, y: x != y), +} + +_constants = { + "e": Constant(value=math.e), + "pi": Constant(value=math.pi), + "tau": Constant(value=math.tau), +} diff --git a/final_task/pycalc/evaluator.py b/final_task/pycalc/evaluator.py new file mode 100644 index 00000000..6b77bfaa --- /dev/null +++ b/final_task/pycalc/evaluator.py @@ -0,0 +1,71 @@ +from . import definitions +import re +from collections import deque + + +class Evaluator(object): + def __init__(self): + self.func = re.compile( + r"(" + + "|".join( + [ + re.escape(func) + for func in sorted( + definitions._functions, key=lambda func: len(func), reverse=True + ) + ] + ) + + ")" + ) + self.num = re.compile(definitions._number) + self.op = re.compile( + r"|".join( + [ + re.escape(op) + for op in sorted( + definitions._operators, key=lambda func: len(func), reverse=True + ) + ] + ) + ) + + def evaluate(self, iExpr): + func = self.func + num = self.num + op = self.op + outputStack = [] + args = [] + lenexpr = len(iExpr) + for token in iExpr: + if num.match(str(token)): + outputStack.append(token) + elif op.match(token): + operand2 = outputStack.pop() + operand1 = outputStack.pop() + outputStack.append( + definitions._operators[token].func(operand1, operand2) + ) + elif token == ",": + outputStack.append(",") + elif func.match(token): + if ( + "," in outputStack + and num.match(str(outputStack[-3])) + and outputStack[-2] == "," + and num.match(str(outputStack[-1])) + ): + operand2 = outputStack.pop() + comma = outputStack.pop() + operand1 = outputStack.pop() + outputStack.append( + definitions._functions[token](operand1, operand2) + ) + else: + operand = outputStack.pop() + outputStack.append(definitions._functions[token](operand)) + if len(outputStack) == 0: + raise ValueError("empty expression") + if len(outputStack) != 1: + raise ValueError("not right nummer of operand") + + return outputStack[0] diff --git a/final_task/setup.py b/final_task/setup.py index e69de29b..bd0a214e 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, find_packages + +setup( + name='pycalc', + version='1.0', + author='Pavel Karshakevich', + author_email='pashakorsh@gmail.com', + packages=find_packages(), + entry_points={'console_scripts': ['pycalc = pycalc.cli:main']}, + description='Pure-python command-line calculator.', + py_modules=['pycalc'] +)