Search code examples
pythonpyparsingjsonlogic

Convert simple expressions into JsonLogic format


Using python, I need to convert expressions into JsonLogic format. Expressions such as Boolean expressions, if else / ternary expressions, etc.

Any suggestions how to achieve this ?

P.S. I see that we have a js-to-json-logic library for the same in Javascript. Could not find its equivalent Python Library.

Example 1:

Input:

((var001 == "Y"))?1:((var001 == "N"))?0:false

Output:

{
"if": [
  {
    "==": [
      {
        "var": "var001"
      },
      "Y"
    ]
  },
  1,
  {
    "if": [
      {
        "==": [
          {
            "var": "var001"
          },
          "N"
        ]
      },
      0,
      false
    ]
  }
]
}

Example 2:

Input:

CustomFunc(var123, "%Y-%d", (var123 == "N" ? 0 : 123))

Note: Input could be a combination of custom function (having n parameters) and any of these parameters could be single attribute or a combination of further expressions.

Output:

{
  "CustomFunc": [
    {
      "var": "var123"
    },
    "%Y-%d",
    {
    "if": [
        {
        "==": [
                {
                    "var": "var123"
                },
                "N"
            ]
        },
        0,
        123
    ]
    }
  ]
}

Example 3:

Input:

9 + 2 - 6 * 4

Output as per opertor precedence and parenthesis


Solution

  • Pyparsing's infixNotation method will permit the definition of unary, binary, and ternary operators (such as your expr ? true_value : false_value operations). This code will parse your given expression:

    import pyparsing as pp
    ppc = pp.common
    
    bool_constant = pp.oneOf("true false")
    integer = ppc.integer()
    ident = ppc.identifier()
    qs = pp.quotedString()
    
    operand = qs | integer | bool_constant | ident
    
    comparison_operator = pp.oneOf("< > >= <=")
    eq_operator = pp.oneOf("== !=")
    
    expr = pp.infixNotation(operand,
                            [
                                (comparison_operator, 2, pp.opAssoc.LEFT),
                                (eq_operator, 2, pp.opAssoc.LEFT),
                                (('?', ':'), 3, pp.opAssoc.LEFT),
                            ])
    
    expr.runTests("""\
                    ((var001 == "Y"))?1:((var001 == "N"))?0:false
                    """
                  )
    

    Having the parser is the first half of the battle. This answer continues on to show how to attach classes to the various parsed terms - in that case it was to evaluate the result, but for you, you'll probably want to do something like implement a as_jsonlogic() method to these classes to emit equivalent forms for the JsonLogic format.

    EDIT:

    Ok, that may not have been that helpful to just show you the parser. So here is the parser with the added classes, and their respective as_jsonlogic() methods.

    import pyparsing as pp
    ppc = pp.common
    
    bool_constant = pp.oneOf("true false")
    integer = ppc.integer()
    ident = ppc.identifier()
    qs = pp.quotedString()
    
    class Node:
        def __init__(self, tokens):
            self._tokens = tokens
            self.assign_vars()
    
        def assign_vars(self):
            pass
    
        def as_jsonlogic(self) -> str:
            raise NotImplementedError()
    
    class Verbatim(Node):
        def as_jsonlogic(self) -> str:
            return str(self._tokens[0])
    
    class Identifier(Node):
        def as_jsonlogic(self) -> str:
            return f'{{ "var": "{self._tokens[0]}" }}'
    
    class Comparison(Node):
        def assign_vars(self):
            self.oper1, self.operator, self.oper2 = self._tokens[0]
    
        def as_jsonlogic(self) -> str:
            return f'{{ "{self.operator}" : [ {self.oper1.as_jsonlogic()}, {self.oper2.as_jsonlogic()} ]  }}'
    
    class Ternary(Node):
        def assign_vars(self):
            self.condition, _, self.true_value, _, self.false_value = self._tokens[0]
    
        def as_jsonlogic(self) -> str:
            return f'{{ "if" : [ {self.condition.as_jsonlogic()}, {self.true_value.as_jsonlogic()}, {self.false_value.as_jsonlogic()} ]  }}'
    
    # add the classes a parse actions, so that each expression gets converted to a Node subclass instance
    qs.add_parse_action(Verbatim)
    integer.add_parse_action(Verbatim)
    bool_constant.add_parse_action(Verbatim)
    ident.add_parse_action(Identifier)
    
    operand = qs | integer | bool_constant | ident
    
    comparison_operator = pp.oneOf("< > >= <=")
    eq_operator = pp.oneOf("== !=")
    
    # add parse actions to each level of the infixNotation    
    expr = pp.infixNotation(operand,
                            [
                                (comparison_operator, 2, pp.opAssoc.LEFT, Comparison),
                                (eq_operator, 2, pp.opAssoc.LEFT, Comparison),
                                (('?', ':'), 3, pp.opAssoc.RIGHT, Ternary),
                            ])
    
    # use runTests to run some tests, with a post_parse function
    # to call as_jsonlogic() on the parsed result
    expr.runTests("""\
        "Y"
        false
        100
        var001
        (var001 == 100)
        ((var001 == "Y"))?1:((var001 == "N"))?0:false
        """, post_parse=lambda s, r: r[0].as_jsonlogic())
    

    Prints:

    "Y"
    "Y"
    
    false
    false
    
    100
    100
    
    var001
    { "var": "var001" }
    
    (var001 == 100)
    { "==" : [ { "var": "var001" }, 100 ]  }
    
    ((var001 == "Y"))?1:((var001 == "N"))?0:false
    { "if" : [ { "==" : [ { "var": "var001" }, "Y" ]  }, 1, { "if" : [ { "==" : [ { "var": "var001" }, "N" ]  }, 0, false ]  } ]  }
    

    I'll leave the pretty indentation to you.