javascriptarraysconditional-operatorbooleanquery

How to store multilevel conditions in an array


I want to code a function to handle definable conditions.

The reference data is contained in an object and a simple condition is stored in a 3 elements array like this :

["name", "==", "John Doe"]

here is the code that works well to test a simple condition:

function getResultOfSimpleCondition(data, condition) {
    let c1 = data[condition[0]],
        operateur = condition[1],     
        c2 = condition[2], cond=true;
        switch(operateur){
                case "==" :
                case "="  :         cond = (c1 == c2 ); break;
                case "!=" :         cond = (c1 != c2 ); break;
                case ">"  :         cond = (c1 >  c2 ); break;
                case "<"  :         cond = (c1 <  c2 ); break;
                case ">=" :         cond = (c1 >= c2 ); break;
                case "<=" :         cond = (c1 <= c2 ); break;
                case "like":        cond = (c1.indexOf(c2) >  -1); break;
                case "not like":    cond = (c1.indexOf(c2) == -1); break;
                default   :         cond = (c1 == c2 ); break;
        }
    return cond
}

let myData = { name:'John Doe', age:'28', town:'PARIS', qty:5, uptodate: true},

    condition_0 = ["name", "==", "Jack Sparrow"],    // result false
    condition_1 = ["age", ">=", "24"],               // result true
    condition_2 = ["uptodate", "==", false],         // result false
    condition_3 = ["town", "==", "PARIS"];           // result true

console.log( getResultOfSimpleCondition(myData, condition_0) )

what I'm looking for is how to implement more complex conditions on the same principle.

For example:

on 2 levels:

[ condition_0, "OR", condition_1 ] // result true

or

[ condition_1, "AND", condition_2 ] // result false

on more levels:

[[ condition_0, "OR", condition_1 ], "AND", condition_3] // result true

or

[[ condition_0, "OR", condition_1 ], "AND", condition_3, "AND NOT", [condition_5, "OR", condition_23 ] ]

the code would look like

let myData = { name:'John Doe', age:'28', town:'PARIS', qty:5, uptodate: true},
    complexCondition = [[ condition_0, "OR", condition_1 ], "AND", condition_3, "AND NOT", [condition_5, "OR", condition_23 ] ];

function getResultOfComplexCondition(data, condition){
...
}

console.log( getResultOfComplexCondition(myData, complexCondition) )

thank you in advance


Solution

  • I simplified your expression a bit, but to demonstrate a recursive walk through an AST (abstract syntax tree). I used Acorn to parse a simplified version of your provided expressions.

    In regards to your 'like' and 'not like', you will need to devise your own grammar. See "Abstract Syntax Trees with a Recursive Descent Parser" for tokenization logic using regular expressions.

    const evaluateExpression = (context, expression) => {
      const visitor = new NodeVisitor({ context });
      const ast = acorn.parse(expression, { ecmaVersion: '2020' });
      //console.log(ast);
      return visitor.visit(ast.body[0].expression);
    };
    
    const main = () => {
      const myData = { name: 'John Doe', age: 28, town: 'PARIS', qty: 5, uptodate: true };
      console.log(evaluateExpression(myData, 'age >= 24 && town == "PARIS"'));
    };
    
    // Adapted from:
    // https://inspirnathan.com/posts/163-abstract-syntax-trees-with-recursive-descent-parser/
    class NodeVisitor {
      constructor({ context }) {
        this.context = context;
      }
    
      visit(node) {
        switch (node.type) {
          case 'Literal':
            return this.visitLiteral(node);
          case 'Identifier':
            return this.visitIdentifier(node);
          case 'BinaryExpression':
            return this.visitBinaryExpression(node);
          case 'LogicalExpression':
            return this.visitLogicalExpression(node);
        }
      }
    
      visitLiteral(node) {
        return node.value;
      }
    
      visitIdentifier(node) {
        return node.name;
      }
    
      visitBinaryExpression(node) {
        switch (node.operator) {
          case '<':
            return this.context[this.visit(node.left)] < this.visit(node.right);
          case '<=':
            return this.context[this.visit(node.left)] <= this.visit(node.right);
          case '>':
            return this.context[this.visit(node.left)] > this.visit(node.right);
          case '>=':
            return this.context[this.visit(node.left)] >= this.visit(node.right);
          case '==':
            return this.context[this.visit(node.left)] === this.visit(node.right);
          case '!=':
            return this.context[this.visit(node.left)] !== this.visit(node.right);
          default:
            throw new Error(`Invalid operation: ${node.operator}`);
        }
      }
    
      visitLogicalExpression(node) {
        switch (node.operator) {
          case '&&':
            return this.visit(node.left) && this.visit(node.right);
          case '||':
            return this.visit(node.left) || this.visit(node.right);
          default:
            throw new Error(`Invalid operation: ${node.operator}`);
        }
      }
    }
    
    main();
    .as-console-wrapper { top: 0; max-height: 100% !important; }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/8.8.2/acorn.min.js"></script>
    <!--
    // Acorn AST of parsed expression:
    {
      "type": "Program",
      "start": 0,
      "end": 28,
      "body": [
        {
          "type": "ExpressionStatement",
          "start": 0,
          "end": 28,
          "expression": {
            "type": "LogicalExpression",
            "start": 0,
            "end": 28,
            "left": {
              "type": "BinaryExpression",
              "start": 0,
              "end": 9,
              "left": {
                "type": "Identifier",
                "start": 0,
                "end": 3,
                "name": "age"
              },
              "operator": ">=",
              "right": {
                "type": "Literal",
                "start": 7,
                "end": 9,
                "value": 24,
                "raw": "24"
              }
            },
            "operator": "&&",
            "right": {
              "type": "BinaryExpression",
              "start": 13,
              "end": 28,
              "left": {
                "type": "Identifier",
                "start": 13,
                "end": 17,
                "name": "town"
              },
              "operator": "==",
              "right": {
                "type": "Literal",
                "start": 21,
                "end": 28,
                "value": "PARIS",
                "raw": "\"PARIS\""
              }
            }
          }
        }
      ],
      "sourceType": "script"
    }
    -->