0

After going through several examples of pyparsing and posts from this site, i managed to write the code that will parse the expressions exactly the way i wanted. But now i m stuck with how to evaluate it, and hence seeking your help here.

So when i give this string: "(Number(3) > Number(5)) AND (Time(IST) < Number(1030))" the parse will output me:
[[[['Number', [3]], '>', ['Number', [5]]], 'AND', [['Time', ['IST']], '<', ['Number', [1030]]]]]

Number is a custom function that takes int input and returns int. Time is a custom function that takes string input and returns current system time as int. Like this i will have many custom functions which will take some inputs and return values.

So can someone please help me to evaluate the parsed out put, so finally I should get a True or a False as final result ?

Here is the copy of my python script:

from pyparsing import (
    CaselessKeyword,Suppress,Word,alphas,alphanums,nums,Optional,Group,oneOf,Forward,infixNotation,
    opAssoc,dblQuotedString,delimitedList,Combine,Literal,QuotedString,ParserElement,Keyword,
    OneOrMore,pyparsing_common as ppc,
)

ParserElement.enablePackrat()

LPAR, RPAR = map(Suppress, "()")

expr = Forward()

alps = Word(alphas, alphanums) 

def stat_function(name):
    return ( 
        Group(CaselessKeyword(name) + Group(LPAR + delimitedList(expr) + RPAR)) |
        Group(CaselessKeyword(name) + Group(LPAR + delimitedList(alps) + RPAR))
    )

timeFunc = stat_function("Time")
dateFunc = stat_function("Date")
numFunc  = stat_function("Number")
funcCall = timeFunc | dateFunc | numFunc

Compop = oneOf("< = > >= <= != <>")
 
multOp = oneOf("* /")
addOp = oneOf("+ -")
logicalOp = oneOf("and or AND OR")
numericLiteral = ppc.number

operand = numericLiteral | funcCall | alps  
arithExpr = infixNotation(
    operand, [(multOp, 2, opAssoc.LEFT), 
              (addOp, 2, opAssoc.LEFT),
              (Compop, 2, opAssoc.LEFT),
              (logicalOp, 2, opAssoc.LEFT),
              ]  
)

expr <<= arithExpr  

s1 = "(Number(3) > Number(5)) AND (Time(IST) < Number(1030))"
result = expr.parseString(s1)
print(result)

1 Answers1

3

Having parsed your expression, your work is only half done (if that).

The typical next step, after converting your input string into a nested structure of tokens, is to recursively walk this structure and interpret your markers such as "Number", "AND", etc. and perform the associated functions.

However, doing so will be pretty much just retracing the steps already done by pyparsing, and the generated infixNotation expression.

I recommend defining eval'able node classes for each expression in your grammar, and pass them to pyparsing as parse actions. Then when you are done, you can just call result.eval(). The actual functionality of the evaluation is implemented in each related node class. This way, pyparsing does the construction of your nodes while it parses, instead of you having to do it again after the fact.

Here are the classes for your functions (and I have slightly modified stat_function to accommodate them):

class Node:
    """
    Base class for all of the parsed node classes.
    """
    def __init__(self, tokens):
        self.tokens = tokens[0]

class Function(Node):
    def __init__(self, tokens):
        super().__init__(tokens)
        self.arg = self.tokens[1][0]

class Number(Function):
    def eval(self):
        return self.arg

class Time(Function):
    def eval(self):
        from datetime import datetime
        if self.arg == "IST":
            return int(datetime.now().strftime("%H%M"))
        else:
            return 0

class Date(Function):
    def eval(self):
        return 0

def stat_function(name, eval_fn):
    return ( 
        Group(CaselessKeyword(name) + Group(LPAR + delimitedList(expr) + RPAR)) |
        Group(CaselessKeyword(name) + Group(LPAR + delimitedList(alps) + RPAR))
    ).addParseAction(eval_fn)

timeFunc = stat_function("Time", Time)
dateFunc = stat_function("Date", Date)
numFunc  = stat_function("Number", Number)
funcCall = timeFunc | dateFunc | numFunc

(I had to guess at what "Time(IST)" was supposed to do, but since you were comparing to 1030, I assumed it would return 24-hour HHMM time as an int.)

Now you can parse these simple operands and evaluate them:

s1 = "Number(27)"
s1 = "Time(IST)"
result = expr.parseString(s1)
print(result)
print(result[0].eval())

The same now goes for your logical, comparison, and arithmetic nodes:

class Logical(Node):
    def eval(self):
        # do not make this a list comprehension, else
        # you will defeat any/all short-circuiting
        eval_exprs = (t.eval() for t in self.tokens[::2])
        return self.logical_fn(eval_exprs)

class AndLogical(Logical):
    logical_fn = all
    
class OrLogical(Logical):
    logical_fn = any

class Comparison(Node):
    op_map = {
        '<': operator.lt,
        '>': operator.gt,
        '=': operator.eq,
        '!=': operator.ne,
        '<>': operator.ne,
        '>=': operator.ge,
        '<=': operator.le,
    }
    def eval(self):
        op1, op, op2 = self.tokens
        comparison_fn = self.op_map[op]
        return comparison_fn(op1.eval(), op2.eval())

class BinArithOp(Node):
    op_map = {
        '*': operator.mul,
        '/': operator.truediv,
        '+': operator.add,
        '-': operator.sub,
    }
    def eval(self):
        # start by eval()'ing the first operand
        ret = self.tokens[0].eval()

        # get following operators and operands in pairs
        ops = self.tokens[1::2]
        operands = self.tokens[2::2]
        for op, operand in zip(ops, operands):
            # update cumulative value by add/subtract/mult/divide the next operand
            arith_fn = self.op_map[op]
            ret = arith_fn(ret, operand.eval())
        return ret

operand = numericLiteral | funcCall | alps  

Each operator level to infixNotation takes an optional fourth argument, which is used as a parse action for that operation. (I also broke out AND and OR as separate levels, as AND's are typically evaluated at higher precedence than OR).

arithExpr = infixNotation(
    operand, [(multOp, 2, opAssoc.LEFT, BinArithOp), 
              (addOp, 2, opAssoc.LEFT, BinArithOp),
              (Compop, 2, opAssoc.LEFT, Comparison),
              (CaselessKeyword("and"), 2, opAssoc.LEFT, AndLogical),
              (CaselessKeyword("or"), 2, opAssoc.LEFT, OrLogical),
              ]  
)

expr <<= arithExpr

Now to evaluate your input string:

s1 = "(Number(3) > Number(5)) AND (Time(IST) < Number(1030))"
result = expr.parseString(s1)
print(result)
print(result[0].eval())

Prints:

[<__main__.AndLogical object at 0xb64bc370>]
False

If you change s1 to compare 3 < 5 (and run this example before 10:30am):

s1 = "(Number(3) < Number(5)) AND (Time(IST) < Number(1030))"

you get:

[<__main__.AndLogical object at 0xb6392110>]
True

There are similar examples in the pyparsing examples directory, search for those using infixNotation.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130