Advertisement
Guest User

Calculator.py

a guest
Dec 4th, 2012
164
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.08 KB | None | 0 0
  1. """calculator.py
  2.  
  3. Provides a method (calc(evalstring, variablesdict) that evaluates expressions such as (1+1)*8, 1d12 and sin(2*pi).
  4. """
  5.  
  6. import math
  7. import random
  8. import string
  9. import re
  10.  
  11. class Fixity:
  12.     """Enumeration specifying whether an operator is prefix (V2), postfix (2!), infix (2+2) or only used as a function (atan2(0,pi)."""
  13.     Prefix = 1
  14.     Postfix = 2
  15.     Infix = 4
  16.     FunctionOnly = 8
  17.    
  18.     fixitynames = {
  19.     Prefix : "Prefix",
  20.     Postfix : "Postfix",
  21.     Infix : "Infix",
  22.     FunctionOnly : "Function",
  23.     }
  24.  
  25. def fixityname(fixity):
  26.     if fixity in Fixity.fixitynames:
  27.         return Fixity.fixitynames[fixity]
  28.     return "Buggy"
  29.  
  30. # regular expressions
  31.  
  32. regexdouble = r"(?<!\d)-?(\d*\.)?\d+([eE][+\-]?\d+)?|[nN]a[nN]|-?[iI]nf(inity)?"
  33. # two noteworthy things - a look behind so that the first -5 in 5-5*-5 can't be matched (and instead 5 will be matched for 5*-5 and later a subtraction will be performed of 5--25) and the allowance for scientific notation, nans and +/- infinity
  34. regexdoublecompiled = re.compile(regexdouble)
  35. regexstraybrackets = re.compile(r"[()]")
  36.  
  37. regexbinaryinfix = r"(?P<num1>"+regexdouble+")\s*{}\s*(?P<num2>"+regexdouble+")"
  38. # {} is where operator is substituted
  39. regexunaryprefix = r"(?<!\d){}(?P<num>"+regexdouble+")"
  40. # lookbehind to prevent two numbers from getting mashed together
  41. regexunarypostfix = r"(?P<num>"+regexdouble+"){}(?![\d.])"
  42. # lookahead, similar reason
  43. regexbrackets = re.compile(r"(?P<funcname>(\b[a-zA-Z]\w*)?)\((?P<bracketed>[^(]*?)\)")
  44. # brackets with an optional function name. function name must start with a letter than proceed with letters/numbers
  45.  
  46. def sanitize(s = ""):
  47.     """Sanitizes all regex metacharacters to be treated as literals."""
  48.     # [\^$.|?*+()
  49.     return re.sub(r"([\[\\\^\$\.\|\?\*\+\(\)])", r"\\\1", s)
  50.  
  51. class Operator:
  52.     """An object defining an operator - its operator name, its name when occuring as a function, its precedence (how early in a calculation it is applied), its fixity, the function called on its arguments and the regex used to search for it."""
  53.     def __init__(self, operatorname = "", functionname = "", precedence = 1, fixity = Fixity.FunctionOnly, func = None):
  54.         self.operatorname = operatorname
  55.         self.functionname = functionname
  56.         self.precedence = precedence
  57.         self.fixity = fixity
  58.         self.func = func
  59.         if self.fixity == Fixity.Infix:
  60.             self.regex = re.compile(regexbinaryinfix.format(sanitize(self.operatorname)), re.IGNORECASE)
  61.         elif self.fixity == Fixity.Prefix:
  62.             self.regex = re.compile(regexunaryprefix.format(sanitize(self.operatorname)), re.IGNORECASE)
  63.         elif self.fixity == Fixity.Postfix:
  64.             self.regex = re.compile(regexunarypostfix.format(sanitize(self.operatorname)), re.IGNORECASE)
  65.         elif self.fixity == Fixity.FunctionOnly:
  66.             self.regex = None
  67.    
  68.     def __repr__(self):
  69.         return "Operator('{}', '{}', {}, {}, {})".format(self.operatorname, self.functionname, self.precedence, self.fixity, self.func)
  70.    
  71.     def __str__(self):
  72.         if self.fixity == Fixity.FunctionOnly:
  73.             return "Function {}()".format(self.functionname)
  74.         elif self.fixity == Fixity.Infix:
  75.             return "{} binary operator {} / binary function {}()".format(fixityname(self.fixity), self.operatorname, self.functionname)
  76.         else:
  77.             return "{} unary operator {} / unary function {}()".format(fixityname(self.fixity), self.operatorname, self.functionname)
  78.    
  79.     def __cmp__(self, other):
  80.         return cmp(self.precedence, other.precedence)
  81.  
  82. binaryoperators = [
  83.     Operator('d', 'roll', 50, Fixity.Infix, lambda x, y: reduce (lambda x, y: x + y, [random.randint(1, int(y)) for i in range(int(x))])),
  84.     Operator('avg', 'avg', 40, Fixity.Infix, lambda x, y: (x + y)/ 2),
  85.     Operator('min', 'min', 40, Fixity.Infix, lambda x, y: min(x, y)),
  86.     Operator('max', 'max', 40, Fixity.Infix, lambda x, y: max(x, y)),
  87.     Operator('^', 'pow', 30, Fixity.Infix, lambda x, y: x ** y),
  88.     Operator('*', 'mul', 20, Fixity.Infix, lambda x, y: x * y),
  89.     Operator('/', 'div', 20, Fixity.Infix, lambda x, y: x / y),
  90.     Operator('%', 'mod', 20, Fixity.Infix, lambda x, y: x % y),
  91.     Operator('+', 'add', 10, Fixity.Infix, lambda x, y: x + y),
  92.     Operator('-', 'sub', 10, Fixity.Infix, lambda x, y: x - y),
  93.     Operator('log', 'log', 1, Fixity.FunctionOnly, lambda x, y: math.log(x, y)),
  94.     Operator('atan2', 'atan2', 1, Fixity.FunctionOnly, lambda x, y: math.atan2(x, y)),
  95.     Operator('gauss', 'gauss', 1, Fixity.FunctionOnly, lambda x, y: random.gauss(x, y)),
  96. ]
  97.  
  98. # new operators should not be added without doing this sort
  99. binaryoperators.sort(cmp = lambda x,y: cmp(x.precedence, y.precedence), reverse = True)
  100.  
  101. unaryoperators = [
  102.     Operator('rand', 'rand', 25, Fixity.Prefix, lambda x: random.uniform(0, x)),
  103.     Operator('!', 'factorial', 20, Fixity.Postfix, lambda x: math.gamma(x+1)),
  104.     Operator('ln', 'ln', 15, Fixity.Prefix, lambda x: math.log(x)),
  105.     Operator('abs', 'abs', 15, Fixity.Prefix, lambda x: abs(x)),
  106.     Operator('sin', 'sin', 15, Fixity.Prefix, lambda x: math.sin(x)),
  107.     Operator('cos', 'cos', 15, Fixity.Prefix, lambda x: math.cos(x)),
  108.     Operator('tan', 'tan', 15, Fixity.Prefix, lambda x: math.tan(x)),
  109.     Operator('asin', 'asin', 15, Fixity.Prefix, lambda x: math.asin(x)),
  110.     Operator('acos', 'acos', 15, Fixity.Prefix, lambda x: math.acos(x)),
  111.     Operator('atan', 'atan', 15, Fixity.Prefix, lambda x: math.atan(x)),
  112.     Operator('sinh', 'sinh', 15, Fixity.Prefix, lambda x: math.sinh(x)),
  113.     Operator('cosh', 'cosh', 15, Fixity.Prefix, lambda x: math.cosh(x)),
  114.     Operator('tanh', 'tanh', 15, Fixity.Prefix, lambda x: math.tanh(x)),
  115.     Operator('asinh', 'asinh', 15, Fixity.Prefix, lambda x: math.asinh(x)),
  116.     Operator('acosh', 'acosh', 15, Fixity.Prefix, lambda x: math.acosh(x)),
  117.     Operator('atanh', 'atanh', 15, Fixity.Prefix, lambda x: math.atanh(x)),
  118.     Operator('V', 'sqrt', 10, Fixity.Prefix, lambda x: math.sqrt(x)),
  119.     Operator('floor', 'floor', 1, Fixity.FunctionOnly, lambda x: math.floor(x)),
  120.     Operator('ceil', 'ceil', 1, Fixity.FunctionOnly, lambda x: math.ceil(x)),
  121.     Operator('round', 'round', 1, Fixity.FunctionOnly, lambda x: round(x)),
  122. ]
  123.  
  124. # new operators should not be added without doing this sort
  125. unaryoperators.sort(cmp = lambda x,y: cmp(x.precedence, y.precedence), reverse = True)
  126.  
  127. # scope variables to object instead of global, to eliminate need to reset manually?
  128. variables = {
  129.     'pi': math.pi,
  130.     'e': math.e,
  131.     'ans': 0,
  132. }
  133.  
  134. def resetvariables():
  135.     """Deletes all variables in variables except for pi, e and ans."""
  136.     variables = {
  137.         'pi': math.pi,
  138.         'e': math.e,
  139.         'ans': 0,
  140.     }
  141.  
  142. def calc(evalstring = "", variablesdict = {}):
  143.     """Evaluates an expression such as (1+1)*8, 1d12 and sin(2*pi). Can take a dictionary of variables (string -> float), which can then be used in the expression. Returns a float or throws an exception if it couldn't do it."""
  144.     for (k, v) in variablesdict.iteritems():
  145.         variables[string.lower(k)] = v
  146.     ans = float(calccore(evalstring)) # try/catch exceptions?
  147.     variables['ans'] = ans
  148.     return ans
  149.  
  150. def handlebinaryoperators(evalstring = "", firstopindex = -1, lastopindex = -1):
  151.     somethinghappened = True
  152.     while somethinghappened:
  153.         bestmatch = None
  154.         bestoperator = None
  155.         somethinghappened = False
  156.         for operator in binaryoperators[firstopindex:lastopindex+1]:
  157.             if operator.fixity & Fixity.FunctionOnly != 0:
  158.                 continue
  159.             if evalstring.find(operator.operatorname) == -1:
  160.                 continue
  161.             if operator.fixity & Fixity.Infix != 0:
  162.                 mo = operator.regex.search(evalstring)
  163.                 if mo is None:
  164.                     continue
  165.                 elif bestmatch is None or mo.start(0) < bestmatch.start(0):
  166.                     somethinghappened = True
  167.                     bestmatch = mo
  168.                     bestoperator = operator
  169.         if somethinghappened:
  170.             result = bestoperator.func(float(bestmatch.group('num1')), float(bestmatch.group('num2')))
  171.             evalstring = evalstring[0:bestmatch.start(0)] + str(result) + evalstring[bestmatch.end(0):len(evalstring)]
  172.     return evalstring
  173.  
  174. def calccore(evalstring = "", functionname = ""):
  175.     """Internal, do not use"""
  176.     evalstring = evalstring.strip()
  177.     functionname = string.lower(functionname)
  178.    
  179.     # step 1. resolve multiple arguments and function call bodies
  180.     # also, if all we have is a number we can just return
  181.     expectedcommanumber = 0
  182.     if len(functionname) > 0:
  183.         if functionname in [x.functionname for x in binaryoperators]:
  184.             expectedcommanumber = 1
  185.             operatorentry = [x for x in binaryoperators if x.functionname == functionname][0]
  186.         elif functionname in [x.functionname for x in unaryoperators]:
  187.             expectedcommanumber = 0
  188.             operatorentry = [x for x in unaryoperators if x.functionname == functionname][0]
  189.         else:
  190.             raise ArithmeticError("no definition for function name {}".format(functionname))
  191.     else:
  192.         mo = regexdoublecompiled.match(evalstring)
  193.         if mo is not None and mo.end(0) == len(evalstring):
  194.             return evalstring
  195.    
  196.     if expectedcommanumber == 1:
  197.         splitstrings = evalstring.split(",")
  198.         if len(splitstrings) < 2:
  199.             raise ArithmeticError("Passed too few arguments to two valued function: {}({})".format(functionname, evalstring))
  200.         elif len(splitstrings) > 2:
  201.             raise ArithmeticError("Passed too many arguments to two valued function: {}({})".format(functionname, evalstring))
  202.         return operatorentry.func(float(calccore(splitstrings[0])), float(calccore(splitstrings[1])))
  203.     elif len(functionname) > 0 and expectedcommanumber == 0:
  204.         return operatorentry.func(float(calccore(evalstring)))
  205.    
  206.     # step 2: recurse on brackets + do functions, substring and paste back into evalstring
  207.     while True:
  208.         mo = regexbrackets.search(evalstring)
  209.         if mo is None:
  210.             bracketsmo = regexstraybrackets.search(evalstring)
  211.             if bracketsmo is not None:
  212.                 raise ArithmeticError("Unbalanced brackets: Too many {}s.".format(bracketsmo.group(0)))
  213.             else:
  214.                 break
  215.         else:
  216.             result = calccore(mo.group('bracketed'), mo.group('funcname'))
  217.             evalstring = evalstring[0:mo.start(0)] + str(result) + evalstring[mo.end(0):len(evalstring)]
  218.    
  219.     # step 3: replace variables with values
  220.     for (k, v) in sorted(variables.iteritems(), lambda x, y: cmp(len(x[0]), len(y[0])), reverse = True):
  221.         evalstring = re.sub(r"\b{}\b".format(k), str(v), evalstring, re.IGNORECASE)
  222.    
  223.     # step 4: do all unary operators
  224.     # gotta handle precedence, fixity...
  225.     # we're assuming they're sorted from highest to lowest precedence
  226.     # need to loop until nothing happens
  227.     somethinghappened = True
  228.     while somethinghappened:
  229.         somethinghappened = False
  230.         for operator in unaryoperators:
  231.             while True:
  232.                 if operator.fixity & Fixity.FunctionOnly != 0:
  233.                     break
  234.                 if evalstring.find(operator.operatorname) == -1:
  235.                     break
  236.                 if operator.fixity & Fixity.Prefix != 0:
  237.                     mo = operator.regex.search(evalstring)
  238.                 elif operator.fixity & Fixity.Postfix != 0:
  239.                     mo = operator.regex.search(evalstring)
  240.                 if mo is None:
  241.                     break
  242.                 else:
  243.                     result = operator.func(float(mo.group('num')))
  244.                     evalstring = evalstring[0:mo.start(0)] + str(result) + evalstring[mo.end(0):len(evalstring)]
  245.                     somethinghappened = True
  246.    
  247.     # step 5: do all binary operators
  248.     # gotta handle precedence, fixity...
  249.     # while at a precedence level, we have to do all operators from leftmost to rightmost until we run out - to preserve correct order of operation
  250.    
  251.     # assume sorted from highest to lowest precedence
  252.     currentprecedence = 100
  253.     firstopindex = 0
  254.     lastopindex = -1
  255.     for i in range(len(binaryoperators)):
  256.         if binaryoperators[i].precedence < currentprecedence:
  257.             if lastopindex != -1:
  258.                 evalstring = handlebinaryoperators(evalstring, firstopindex, lastopindex)
  259.             firstopindex = lastopindex = i
  260.             currentprecedence = binaryoperators[i].precedence
  261.         else:
  262.             lastopindex = i
  263.             continue
  264.    
  265.     # step 6: if assignment operators (= += etc) are implemented, parse here
  266.    
  267.     # step 7: return result
  268.     return evalstring
  269.  
  270. def test_calc_1():
  271.     epsilon = 0.000000001
  272.     expectations = [("1+1", 2.0),
  273.                     (" 1 + (1) + ( 1 ) + ( 1 + 1 )+(1+1)", 7.0),
  274.                     ("8d1", 8.0),
  275.                     ("abs(-1)", 1.0),
  276.                     ("pi*e", 8.53973422268),
  277.                     ("6-3*4/5+2^2", 7.6),
  278.                     ("1+2-3*4/5%(6-4)", 2.6),
  279.                     ("max(sub(2,pi),mod(3,4))", 3.0),
  280.                     ("1e+03+1e-03", 1000.001),
  281.                     ("4-5*-5", 29.0),
  282.                     #("Infinity-infinity", float("NaN")),
  283.                     ("-5-5-5-5+5-5", -20.0),
  284.                     ("5--5", 10.0),
  285.                     ("4!/((4/2)!*(4/2)!)", 6.0),
  286.                     ("abs(-5*(sin(-5)*cos(-5)))*-5", -6.8002638861),
  287.                     ("V5!+-V5!--V5!", 10.9544511501),
  288.                     ("sin(sin(sin(sin(3))))", 0.13973004691078),
  289.                     ("(5)+abs(5*abs(-5))", 30.0),
  290.                     ("randV0", 0.0),
  291.                     ]
  292.     for (expr, answer) in expectations:
  293.         assert abs(calc(expr) - answer) < epsilon
  294.  
  295. if __name__ == "__main__":
  296.     test_calc_1()
  297.     import timeit
  298.     print 'calc("1") ', timeit.timeit('calc("1")', setup="from __main__ import calc", number=10000)
  299.     print 'eval("1") ', timeit.timeit('eval("1")', number=10000)
  300.     print '1 ', timeit.timeit('1', number=10000)
  301.     print 'calc("1+1") ', timeit.timeit('calc("1+1")', setup="from __main__ import calc", number=10000)
  302.     print 'eval("1+1") ', timeit.timeit('eval("1+1")', number=10000)
  303.     print '1+1 ', timeit.timeit('1+1', number=10000)
  304.     print 'calc("1+2-3*4/5*6-7+8") ', timeit.timeit('calc("1+2-3*4/5*6-7+8")', setup="from __main__ import calc", number=10000)
  305.     print 'eval("1+2-3*4/5*6-7+8") ', timeit.timeit('eval("1+2-3*4/5*6-7+8")', number=10000)
  306.     print '1+2-3*4/5*6-7+8 ', timeit.timeit('1+2-3*4/5*6-7+8', number=10000)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement