# eval_arith.py # # Copyright 2009, 2011 Paul McGuire # # Expansion on the pyparsing example simpleArith.py, to include evaluation # of the parsed tokens. # # Added support for exponentiation, using right-to-left evaluation of # operands # from pyparsing import Word, nums, alphas, Combine, oneOf, \ opAssoc, infixNotation, Literal class EvalConstant(object): "Class to evaluate a parsed constant or variable" vars_ = {} def __init__(self, tokens): self.value = tokens[0] def eval(self): if self.value in EvalConstant.vars_: return EvalConstant.vars_[self.value] else: return float(self.value) class EvalSignOp(object): "Class to evaluate expressions with a leading + or - sign" def __init__(self, tokens): self.sign, self.value = tokens[0] def eval(self): mult = {'+':1, '-':-1}[self.sign] return mult * self.value.eval() def operatorOperands(tokenlist): "generator to extract operators and operands in pairs" it = iter(tokenlist) while 1: try: yield (next(it), next(it)) except StopIteration: break class EvalPowerOp(object): "Class to evaluate multiplication and division expressions" def __init__(self, tokens): self.value = tokens[0] def eval(self): res = self.value[-1].eval() for val in self.value[-3::-2]: res = val.eval()**res return res class EvalMultOp(object): "Class to evaluate multiplication and division expressions" def __init__(self, tokens): self.value = tokens[0] def eval(self): prod = self.value[0].eval() for op,val in operatorOperands(self.value[1:]): if op == '*': prod *= val.eval() if op == '/': prod /= val.eval() return prod class EvalAddOp(object): "Class to evaluate addition and subtraction expressions" def __init__(self, tokens): self.value = tokens[0] def eval(self): sum = self.value[0].eval() for op,val in operatorOperands(self.value[1:]): if op == '+': sum += val.eval() if op == '-': sum -= val.eval() return sum class EvalComparisonOp(object): "Class to evaluate comparison expressions" opMap = { "<" : lambda a,b : a < b, "<=" : lambda a,b : a <= b, ">" : lambda a,b : a > b, ">=" : lambda a,b : a >= b, "!=" : lambda a,b : a != b, "=" : lambda a,b : a == b, "LT" : lambda a,b : a < b, "LE" : lambda a,b : a <= b, "GT" : lambda a,b : a > b, "GE" : lambda a,b : a >= b, "NE" : lambda a,b : a != b, "EQ" : lambda a,b : a == b, "<>" : lambda a,b : a != b, } def __init__(self, tokens): self.value = tokens[0] def eval(self): val1 = self.value[0].eval() for op,val in operatorOperands(self.value[1:]): fn = EvalComparisonOp.opMap[op] val2 = val.eval() if not fn(val1,val2): break val1 = val2 else: return True return False # define the parser integer = Word(nums) real = Combine(Word(nums) + "." + Word(nums)) variable = Word(alphas,exact=1) operand = real | integer | variable signop = oneOf('+ -') multop = oneOf('* /') plusop = oneOf('+ -') expop = Literal('**') # use parse actions to attach EvalXXX constructors to sub-expressions operand.setParseAction(EvalConstant) arith_expr = infixNotation(operand, [ (signop, 1, opAssoc.RIGHT, EvalSignOp), (expop, 2, opAssoc.LEFT, EvalPowerOp), (multop, 2, opAssoc.LEFT, EvalMultOp), (plusop, 2, opAssoc.LEFT, EvalAddOp), ]) comparisonop = oneOf("< <= > >= != = <> LT GT LE GE EQ NE") comp_expr = infixNotation(arith_expr, [ (comparisonop, 2, opAssoc.LEFT, EvalComparisonOp), ]) def main(): # sample expressions posted on comp.lang.python, asking for advice # in safely evaluating them rules=[ '( A - B ) = 0', '(A + B + C + D + E + F + G + H + I) = J', '(A + B + C + D + E + F + G + H) = I', '(A + B + C + D + E + F) = G', '(A + B + C + D + E) = (F + G + H + I + J)', '(A + B + C + D + E) = (F + G + H + I)', '(A + B + C + D + E) = F', '(A + B + C + D) = (E + F + G + H)', '(A + B + C) = (D + E + F)', '(A + B) = (C + D + E + F)', '(A + B) = (C + D)', '(A + B) = (C - D + E - F - G + H + I + J)', '(A + B) = C', '(A + B) = 0', '(A+B+C+D+E) = (F+G+H+I+J)', '(A+B+C+D) = (E+F+G+H)', '(A+B+C+D)=(E+F+G+H)', '(A+B+C)=(D+E+F)', '(A+B)=(C+D)', '(A+B)=C', '(A-B)=C', '(A/(B+C))', '(B/(C+D))', '(G + H) = I', '-0.99 LE ((A+B+C)-(D+E+F+G)) LE 0.99', '-0.99 LE (A-(B+C)) LE 0.99', '-1000.00 LE A LE 0.00', '-5000.00 LE A LE 0.00', 'A < B', 'A < 7000', 'A = -(B)', 'A = C', 'A = 0', 'A GT 0', 'A GT 0.00', 'A GT 7.00', 'A LE B', 'A LT -1000.00', 'A LT -5000', 'A LT 0', 'A=(B+C+D)', 'A=B', 'I = (G + H)', '0.00 LE A LE 4.00', '4.00 LT A LE 7.00', '0.00 LE A LE 4.00 LE E > D', '2**2**(A+3)', ] vars_={'A': 0, 'B': 1.1, 'C': 2.2, 'D': 3.3, 'E': 4.4, 'F': 5.5, 'G': 6.6, 'H':7.7, 'I':8.8, 'J':9.9} # define tests from given rules tests = [] for t in rules: t_orig = t t = t.replace("=","==") t = t.replace("EQ","==") t = t.replace("LE","<=") t = t.replace("GT",">") t = t.replace("LT","<") t = t.replace("GE",">=") t = t.replace("LE","<=") t = t.replace("NE","!=") t = t.replace("<>","!=") tests.append( (t_orig,eval(t,vars_)) ) # copy vars_ to EvalConstant lookup dict EvalConstant.vars_ = vars_ failed = 0 for test,expected in tests: ret = comp_expr.parseString(test)[0] parsedvalue = ret.eval() print(test, expected, parsedvalue) if parsedvalue != expected: print("<<< FAIL") failed += 1 else: print('') print('') if failed: print(failed, "tests FAILED") else: print("all tests PASSED") if __name__=='__main__': main()