How to store multilevel conditions in an array

64 Views Asked by At

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

1

There are 1 best solutions below

0
Mr. Polywhirl On BEST ANSWER

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"
}
-->