0

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

  • in the same way as with the getResultOfSimpleCondition function, I want to be able to solve more complex boolean expressions (conditions). I put some examples above – Lionel KAISER Apr 20 '23 at 18:34
  • 1
    For a "readable" solution, it seems like recursion might work here. A function that goes through every child (or child of child, etc) array in the complex conditions until it finds only string elements, calls your simpleConditions function, then builds those results back up the condition tree to the top level. That won't be very performant though, probably — you might want to investigate building a little rules engine. – Zac Anger Apr 20 '23 at 18:55
  • You most probably wants to implement your "=" and "!=" by using the actual javascript operators `===` and `!==` instead of `==` and `!=`, to avoid pitfalls like `0 == false` – Rodrigo Rodrigues Apr 20 '23 at 19:58

1 Answers1

1

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"
}
-->
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132