Skip to content

Instantly share code, notes, and snippets.

@cynici
Last active February 7, 2018 04:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cynici/5865326 to your computer and use it in GitHub Desktop.
Save cynici/5865326 to your computer and use it in GitHub Desktop.
Using pyparsing to reduce expressions into a single boolean value. An expression may contain simple arithmetic and keywords (substituted with values from user-given parameter dictionary).
import sys
import traceback
import logging
from pyparsing import *
ParserElement.enablePackrat()
def EvalSignOp(s, l, t):
"Evaluate expressions with a leading + or - sign"
sign, value = t[0]
mult = {'+':1, '-':-1}[sign]
res = mult * value
logging.debug("SIGN: t=%s res=%s" % (t, res))
return res
def operatorOperands(tokenlist):
"generator to extract operators and operands in pairs"
it = iter(tokenlist)
while 1:
try:
o1 = next(it)
o2 = next(it)
yield ( o1, o2 )
except StopIteration:
break
def EvalMultOp(s, l, t):
"Evaluate multiplication and division expressions"
prod = t[0][0]
for op,val in operatorOperands(t[0][1:]):
if op == '*':
prod *= val
if op == '/':
prod /= val
if op == '//':
prod //= val
if op == '%':
prod %= val
logging.debug("MULT: t=%s res=%s" % (t, prod))
return prod
def EvalAddOp(s, l, t):
"Evaluate addition and subtraction expressions"
sum = t[0][0]
for op,val in operatorOperands(t[0][1:]):
if op == '+':
sum += val
if op == '-':
sum -= val
logging.debug("ADD: t=%s res=%s" % (t, sum))
return sum
def EvalComparisonOp(s, l, t):
"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,
"!=" : lambda a,b : a != b,
"<>" : lambda a,b : a != b,
}
res = False
val1 = t[0][0]
for op,val2 in operatorOperands(t[0][1:]):
fn = opMap[op]
if not fn(val1,val2):
break
val1 = val2
else:
res = True
logging.debug("COMP: t=%s res=%s\n" % (t, res))
return res
def EvalAnd(s, l, t):
bool1, op, bool2 = t[0]
res = bool1 and bool2
logging.debug("%s %s %s res=%s" % (bool1, op, bool2, res))
return res
def EvalOr(s, l, t):
bool1, op, bool2 = t[0]
res = bool1 or bool2
logging.debug("%s %s %s res=%s" % (bool1, op, bool2, res))
return res
def EvalNot(s, l, t):
op, bool1 = t[0]
res = not bool1
logging.debug("%s %s res=%s" % (op, bool1, res))
return res
Param = Word(alphas+"_", alphanums+"_")
Qstring = quotedString(r".*").setParseAction(removeQuotes)
PosInt = Word(nums).setParseAction(lambda s,l,t: [int(t[0])])
PosReal = ( Combine(Word(nums) + Optional("." + Word(nums))
+ oneOf("E e") + Optional( oneOf('+ -')) + Word(nums))
| Combine(Word(nums) + "." + Word(nums))
).setParseAction(lambda s,l,t: [float(t[0])])
signop = oneOf('+ -')
multop = oneOf('* / // %')
plusop = oneOf('+ -')
comparisonop = oneOf("< <= > >= = == != <>")
andop = CaselessKeyword("AND")
orop = CaselessKeyword("OR")
notop = CaselessKeyword("NOT")
class AfisParser:
def __init__(self, param_dic={}):
self.param_dic = {}
# Save parameters (key-value pairs), but with capitalized key
for k, v in param_dic.items():
K = k.upper()
if K in self.param_dic:
raise ValueError("Parameter key collision: '%s'" % k)
self.param_dic[K] = v
def setParam(self, s, l, t):
"Replace keywords with actual values using param_dic mapping"
param = t[0].upper()
return self.param_dic.get(param, '')
def eval_filter(self, filter_expr):
keyword = Param.copy()
atom = PosReal | PosInt | Qstring | keyword.setParseAction(self.setParam)
expr = operatorPrecedence(atom, [
(signop, 1, opAssoc.RIGHT, EvalSignOp),
(multop, 2, opAssoc.LEFT, EvalMultOp),
(plusop, 2, opAssoc.LEFT, EvalAddOp),
(comparisonop, 2, opAssoc.LEFT, EvalComparisonOp),
(CaselessKeyword("NOT"), 1, opAssoc.RIGHT, EvalNot),
(CaselessKeyword("AND"), 2, opAssoc.LEFT, EvalAnd),
(CaselessKeyword("OR"), 2, opAssoc.LEFT, EvalOr),
])
return expr.parseString(filter_expr, parseAll=True)[0]
def main():
parameters = {
'FRP': 100,
'satellite': 'A',
}
tests = [
# ( Filter_string, Expected_result )
( "199 / 2 > FRP", False ),
( "5 + 45 * 2 > FRP", False ),
( "-5+5 < FRP", True ),
( "satellite is 'N'", False),
( "FRP - 100 == 0", True),
( "FRP = 1 and satellite = 'T'", False ),
( "FRP <> 1 and not satellite = 'T'", True ),
# packrat speeds up nested expressions tremendously
( "(FRP = 1) and ((satellite = 'T') or (satellite iS 'A'))", False ),
]
logging.basicConfig(level=logging.WARNING)
got_error = 0
ap = AfisParser(parameters)
for filter_expr, expected in tests:
try:
result = ap.eval_filter(filter_expr)
if result != expected:
raise AssertionError("yields %s" % (result))
except Exception, err:
traceback.print_exc(file=sys.stderr)
print "%s: %s" % (filter_expr, err)
got_error += 1
return got_error
if __name__ == "__main__":
sys.exit(main())
@jbohren
Copy link

jbohren commented Nov 5, 2014

This is slick, do you mind if I use code based on this in a BSD-licensed project?

@jvitoroc
Copy link

jvitoroc commented Feb 7, 2018

Hey, I converted this code to version 3 and some parseResults errors came up, so I removed the paramater parseAll from line 137, now the fourth test is not working (("satellite == 'A'", True))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment