Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- """calculator.py
- Provides a method (calc(evalstring, variablesdict) that evaluates expressions such as (1+1)*8, 1d12 and sin(2*pi).
- """
- import math
- import random
- import string
- import re
- class Fixity:
- """Enumeration specifying whether an operator is prefix (V2), postfix (2!), infix (2+2) or only used as a function (atan2(0,pi)."""
- Prefix = 1
- Postfix = 2
- Infix = 4
- FunctionOnly = 8
- fixitynames = {
- Prefix : "Prefix",
- Postfix : "Postfix",
- Infix : "Infix",
- FunctionOnly : "Function",
- }
- def fixityname(fixity):
- if fixity in Fixity.fixitynames:
- return Fixity.fixitynames[fixity]
- return "Buggy"
- # regular expressions
- regexdouble = r"(?<!\d)-?(\d*\.)?\d+([eE][+\-]?\d+)?|[nN]a[nN]|-?[iI]nf(inity)?"
- # 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
- regexdoublecompiled = re.compile(regexdouble)
- regexstraybrackets = re.compile(r"[()]")
- regexbinaryinfix = r"(?P<num1>"+regexdouble+")\s*{}\s*(?P<num2>"+regexdouble+")"
- # {} is where operator is substituted
- regexunaryprefix = r"(?<!\d){}(?P<num>"+regexdouble+")"
- # lookbehind to prevent two numbers from getting mashed together
- regexunarypostfix = r"(?P<num>"+regexdouble+"){}(?![\d.])"
- # lookahead, similar reason
- regexbrackets = re.compile(r"(?P<funcname>(\b[a-zA-Z]\w*)?)\((?P<bracketed>[^(]*?)\)")
- # brackets with an optional function name. function name must start with a letter than proceed with letters/numbers
- def sanitize(s = ""):
- """Sanitizes all regex metacharacters to be treated as literals."""
- # [\^$.|?*+()
- return re.sub(r"([\[\\\^\$\.\|\?\*\+\(\)])", r"\\\1", s)
- class Operator:
- """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."""
- def __init__(self, operatorname = "", functionname = "", precedence = 1, fixity = Fixity.FunctionOnly, func = None):
- self.operatorname = operatorname
- self.functionname = functionname
- self.precedence = precedence
- self.fixity = fixity
- self.func = func
- if self.fixity == Fixity.Infix:
- self.regex = re.compile(regexbinaryinfix.format(sanitize(self.operatorname)), re.IGNORECASE)
- elif self.fixity == Fixity.Prefix:
- self.regex = re.compile(regexunaryprefix.format(sanitize(self.operatorname)), re.IGNORECASE)
- elif self.fixity == Fixity.Postfix:
- self.regex = re.compile(regexunarypostfix.format(sanitize(self.operatorname)), re.IGNORECASE)
- elif self.fixity == Fixity.FunctionOnly:
- self.regex = None
- def __repr__(self):
- return "Operator('{}', '{}', {}, {}, {})".format(self.operatorname, self.functionname, self.precedence, self.fixity, self.func)
- def __str__(self):
- if self.fixity == Fixity.FunctionOnly:
- return "Function {}()".format(self.functionname)
- elif self.fixity == Fixity.Infix:
- return "{} binary operator {} / binary function {}()".format(fixityname(self.fixity), self.operatorname, self.functionname)
- else:
- return "{} unary operator {} / unary function {}()".format(fixityname(self.fixity), self.operatorname, self.functionname)
- def __cmp__(self, other):
- return cmp(self.precedence, other.precedence)
- binaryoperators = [
- 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))])),
- Operator('avg', 'avg', 40, Fixity.Infix, lambda x, y: (x + y)/ 2),
- Operator('min', 'min', 40, Fixity.Infix, lambda x, y: min(x, y)),
- Operator('max', 'max', 40, Fixity.Infix, lambda x, y: max(x, y)),
- Operator('^', 'pow', 30, Fixity.Infix, lambda x, y: x ** y),
- Operator('*', 'mul', 20, Fixity.Infix, lambda x, y: x * y),
- Operator('/', 'div', 20, Fixity.Infix, lambda x, y: x / y),
- Operator('%', 'mod', 20, Fixity.Infix, lambda x, y: x % y),
- Operator('+', 'add', 10, Fixity.Infix, lambda x, y: x + y),
- Operator('-', 'sub', 10, Fixity.Infix, lambda x, y: x - y),
- Operator('log', 'log', 1, Fixity.FunctionOnly, lambda x, y: math.log(x, y)),
- Operator('atan2', 'atan2', 1, Fixity.FunctionOnly, lambda x, y: math.atan2(x, y)),
- Operator('gauss', 'gauss', 1, Fixity.FunctionOnly, lambda x, y: random.gauss(x, y)),
- ]
- # new operators should not be added without doing this sort
- binaryoperators.sort(cmp = lambda x,y: cmp(x.precedence, y.precedence), reverse = True)
- unaryoperators = [
- Operator('rand', 'rand', 25, Fixity.Prefix, lambda x: random.uniform(0, x)),
- Operator('!', 'factorial', 20, Fixity.Postfix, lambda x: math.gamma(x+1)),
- Operator('ln', 'ln', 15, Fixity.Prefix, lambda x: math.log(x)),
- Operator('abs', 'abs', 15, Fixity.Prefix, lambda x: abs(x)),
- Operator('sin', 'sin', 15, Fixity.Prefix, lambda x: math.sin(x)),
- Operator('cos', 'cos', 15, Fixity.Prefix, lambda x: math.cos(x)),
- Operator('tan', 'tan', 15, Fixity.Prefix, lambda x: math.tan(x)),
- Operator('asin', 'asin', 15, Fixity.Prefix, lambda x: math.asin(x)),
- Operator('acos', 'acos', 15, Fixity.Prefix, lambda x: math.acos(x)),
- Operator('atan', 'atan', 15, Fixity.Prefix, lambda x: math.atan(x)),
- Operator('sinh', 'sinh', 15, Fixity.Prefix, lambda x: math.sinh(x)),
- Operator('cosh', 'cosh', 15, Fixity.Prefix, lambda x: math.cosh(x)),
- Operator('tanh', 'tanh', 15, Fixity.Prefix, lambda x: math.tanh(x)),
- Operator('asinh', 'asinh', 15, Fixity.Prefix, lambda x: math.asinh(x)),
- Operator('acosh', 'acosh', 15, Fixity.Prefix, lambda x: math.acosh(x)),
- Operator('atanh', 'atanh', 15, Fixity.Prefix, lambda x: math.atanh(x)),
- Operator('V', 'sqrt', 10, Fixity.Prefix, lambda x: math.sqrt(x)),
- Operator('floor', 'floor', 1, Fixity.FunctionOnly, lambda x: math.floor(x)),
- Operator('ceil', 'ceil', 1, Fixity.FunctionOnly, lambda x: math.ceil(x)),
- Operator('round', 'round', 1, Fixity.FunctionOnly, lambda x: round(x)),
- ]
- # new operators should not be added without doing this sort
- unaryoperators.sort(cmp = lambda x,y: cmp(x.precedence, y.precedence), reverse = True)
- # scope variables to object instead of global, to eliminate need to reset manually?
- variables = {
- 'pi': math.pi,
- 'e': math.e,
- 'ans': 0,
- }
- def resetvariables():
- """Deletes all variables in variables except for pi, e and ans."""
- variables = {
- 'pi': math.pi,
- 'e': math.e,
- 'ans': 0,
- }
- def calc(evalstring = "", variablesdict = {}):
- """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."""
- for (k, v) in variablesdict.iteritems():
- variables[string.lower(k)] = v
- ans = float(calccore(evalstring)) # try/catch exceptions?
- variables['ans'] = ans
- return ans
- def handlebinaryoperators(evalstring = "", firstopindex = -1, lastopindex = -1):
- somethinghappened = True
- while somethinghappened:
- bestmatch = None
- bestoperator = None
- somethinghappened = False
- for operator in binaryoperators[firstopindex:lastopindex+1]:
- if operator.fixity & Fixity.FunctionOnly != 0:
- continue
- if evalstring.find(operator.operatorname) == -1:
- continue
- if operator.fixity & Fixity.Infix != 0:
- mo = operator.regex.search(evalstring)
- if mo is None:
- continue
- elif bestmatch is None or mo.start(0) < bestmatch.start(0):
- somethinghappened = True
- bestmatch = mo
- bestoperator = operator
- if somethinghappened:
- result = bestoperator.func(float(bestmatch.group('num1')), float(bestmatch.group('num2')))
- evalstring = evalstring[0:bestmatch.start(0)] + str(result) + evalstring[bestmatch.end(0):len(evalstring)]
- return evalstring
- def calccore(evalstring = "", functionname = ""):
- """Internal, do not use"""
- evalstring = evalstring.strip()
- functionname = string.lower(functionname)
- # step 1. resolve multiple arguments and function call bodies
- # also, if all we have is a number we can just return
- expectedcommanumber = 0
- if len(functionname) > 0:
- if functionname in [x.functionname for x in binaryoperators]:
- expectedcommanumber = 1
- operatorentry = [x for x in binaryoperators if x.functionname == functionname][0]
- elif functionname in [x.functionname for x in unaryoperators]:
- expectedcommanumber = 0
- operatorentry = [x for x in unaryoperators if x.functionname == functionname][0]
- else:
- raise ArithmeticError("no definition for function name {}".format(functionname))
- else:
- mo = regexdoublecompiled.match(evalstring)
- if mo is not None and mo.end(0) == len(evalstring):
- return evalstring
- if expectedcommanumber == 1:
- splitstrings = evalstring.split(",")
- if len(splitstrings) < 2:
- raise ArithmeticError("Passed too few arguments to two valued function: {}({})".format(functionname, evalstring))
- elif len(splitstrings) > 2:
- raise ArithmeticError("Passed too many arguments to two valued function: {}({})".format(functionname, evalstring))
- return operatorentry.func(float(calccore(splitstrings[0])), float(calccore(splitstrings[1])))
- elif len(functionname) > 0 and expectedcommanumber == 0:
- return operatorentry.func(float(calccore(evalstring)))
- # step 2: recurse on brackets + do functions, substring and paste back into evalstring
- while True:
- mo = regexbrackets.search(evalstring)
- if mo is None:
- bracketsmo = regexstraybrackets.search(evalstring)
- if bracketsmo is not None:
- raise ArithmeticError("Unbalanced brackets: Too many {}s.".format(bracketsmo.group(0)))
- else:
- break
- else:
- result = calccore(mo.group('bracketed'), mo.group('funcname'))
- evalstring = evalstring[0:mo.start(0)] + str(result) + evalstring[mo.end(0):len(evalstring)]
- # step 3: replace variables with values
- for (k, v) in sorted(variables.iteritems(), lambda x, y: cmp(len(x[0]), len(y[0])), reverse = True):
- evalstring = re.sub(r"\b{}\b".format(k), str(v), evalstring, re.IGNORECASE)
- # step 4: do all unary operators
- # gotta handle precedence, fixity...
- # we're assuming they're sorted from highest to lowest precedence
- # need to loop until nothing happens
- somethinghappened = True
- while somethinghappened:
- somethinghappened = False
- for operator in unaryoperators:
- while True:
- if operator.fixity & Fixity.FunctionOnly != 0:
- break
- if evalstring.find(operator.operatorname) == -1:
- break
- if operator.fixity & Fixity.Prefix != 0:
- mo = operator.regex.search(evalstring)
- elif operator.fixity & Fixity.Postfix != 0:
- mo = operator.regex.search(evalstring)
- if mo is None:
- break
- else:
- result = operator.func(float(mo.group('num')))
- evalstring = evalstring[0:mo.start(0)] + str(result) + evalstring[mo.end(0):len(evalstring)]
- somethinghappened = True
- # step 5: do all binary operators
- # gotta handle precedence, fixity...
- # 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
- # assume sorted from highest to lowest precedence
- currentprecedence = 100
- firstopindex = 0
- lastopindex = -1
- for i in range(len(binaryoperators)):
- if binaryoperators[i].precedence < currentprecedence:
- if lastopindex != -1:
- evalstring = handlebinaryoperators(evalstring, firstopindex, lastopindex)
- firstopindex = lastopindex = i
- currentprecedence = binaryoperators[i].precedence
- else:
- lastopindex = i
- continue
- # step 6: if assignment operators (= += etc) are implemented, parse here
- # step 7: return result
- return evalstring
- def test_calc_1():
- epsilon = 0.000000001
- expectations = [("1+1", 2.0),
- (" 1 + (1) + ( 1 ) + ( 1 + 1 )+(1+1)", 7.0),
- ("8d1", 8.0),
- ("abs(-1)", 1.0),
- ("pi*e", 8.53973422268),
- ("6-3*4/5+2^2", 7.6),
- ("1+2-3*4/5%(6-4)", 2.6),
- ("max(sub(2,pi),mod(3,4))", 3.0),
- ("1e+03+1e-03", 1000.001),
- ("4-5*-5", 29.0),
- #("Infinity-infinity", float("NaN")),
- ("-5-5-5-5+5-5", -20.0),
- ("5--5", 10.0),
- ("4!/((4/2)!*(4/2)!)", 6.0),
- ("abs(-5*(sin(-5)*cos(-5)))*-5", -6.8002638861),
- ("V5!+-V5!--V5!", 10.9544511501),
- ("sin(sin(sin(sin(3))))", 0.13973004691078),
- ("(5)+abs(5*abs(-5))", 30.0),
- ("randV0", 0.0),
- ]
- for (expr, answer) in expectations:
- assert abs(calc(expr) - answer) < epsilon
- if __name__ == "__main__":
- test_calc_1()
- import timeit
- print 'calc("1") ', timeit.timeit('calc("1")', setup="from __main__ import calc", number=10000)
- print 'eval("1") ', timeit.timeit('eval("1")', number=10000)
- print '1 ', timeit.timeit('1', number=10000)
- print 'calc("1+1") ', timeit.timeit('calc("1+1")', setup="from __main__ import calc", number=10000)
- print 'eval("1+1") ', timeit.timeit('eval("1+1")', number=10000)
- print '1+1 ', timeit.timeit('1+1', number=10000)
- 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)
- print 'eval("1+2-3*4/5*6-7+8") ', timeit.timeit('eval("1+2-3*4/5*6-7+8")', number=10000)
- 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