user544079
user544079

Reputation: 16629

Evaluate expression tree in Javascript

I have input consisting of nested logical expression objects

Ex:

var obj = {
  'OR': [
      {
        'AND': [
            false, true, true
        ]
      },
      {
        'OR': [
            true, false, false, {
                'AND': [true, true]
            }
        ]
      },
      true
  ]
};

Which is equivalent to ((false && true && true) || (true || false || false || (true && true)) || true)

We need to write a function that calculates this

Approach:

Go to the innermost level and evaluate it first, moving to the top

var expressionEvaluator = function(opArr){
            var hasChildObjects = function(arr){
                if(Array.isArray(arr)){
                    return arr.some(function(obj){
                        return typeof(obj) === 'object';
                    });
                }
                else if(typeof(arr) === 'object'){
                    return true;
                }
            };
            var evaluateArr = function(list, operator){
                var result;
                if(operator === 'AND'){
                    result = true;
                    for(var i = 0; i<list.length; i++){
                        if(!list[i]){
                            result = false;
                        }
                    }
                }
                else if(operator === 'OR'){
                    result = false;
                    for(var i = 0; i<list.length; i++){
                        if(list[i]){
                            result = true;
                        }
                    }
                }
                return result;
            };
            var iterate = function(opArr){
                Object.keys(opArr).forEach(function(k){
                    if(hasChildObjects(opArr[k])){
                        iterate(opArr[k]);
                    }
                    else{
                        opArr = evaluateArr(opArr[k], k);
                    }
                });
            };
            iterate(opArr);
            return result;
        }

I am able to reach the innermost object and evaluate it but not reach back to the topmost level and evaluate the entire expression object.

Upvotes: 7

Views: 1546

Answers (5)

Scott Sauyet
Scott Sauyet

Reputation: 50797

Another variant on the theme here:

const evaluate = (tree) =>
  typeof tree === 'boolean'
    ? tree
  : 'OR' in tree
    ? tree .OR .some (evaluate)
  : 'AND' in tree
    ? tree .AND .every (evaluate)
  : false // or throw an error?

const tree = {OR: [{AND: [false, true, true]}, {OR: [true, false, false, {AND: [true, true]}]}, true]}

console .log (evaluate (tree))

evaluate returns the value supplied if it's a boolean. Otherwise it checks for 'OR' or 'AND' nodes, and handles them appropriately. There is a catch-all at the end for non-well-formed trees. Here I return false, but you might throw an error, return null, or something else.

If you don't need that catch-all, this can be simplified to a one-liner:

const evaluate = (tree) =>
  typeof tree == 'boolean' ? tree: tree.OR ? tree.OR .some (evaluate) : tree.AND .every (evaluate)

But I'm concerned by the tree structure. I always worry when there are objects that can only have one property. It feels like an array is then a better design.

This alternative feels cleaner:

const tree = [
  'OR', 
  [
    ['AND', [false, true, true]], 
    ['OR', [true, false, false, ['AND', [true, true]]]], 
    true
  ]
]

And that could be evaluated with similar code:

const evaluate = (tree) =>
  typeof tree == 'boolean' ? tree : tree [1] [tree [0] === 'OR' ? 'some' : 'every'] (evaluate)

Update: A comment from customcommander pointed out that even this array format is overcomplicated.

If instead, we were dealing with something like this:

const tree = [
  'OR', 
  ['AND', false, true, true], 
  ['OR', true, false, false, ['AND', true, true]], 
  true
]

then we could use a version like this:

const evaluate = (tree) =>
  typeof tree === 'boolean' 
    ? tree 
    : tree .slice (1) [tree [0] === 'OR' ? 'some' : 'every'] (evaluate)

Upvotes: 4

customcommander
customcommander

Reputation: 18901

Not sure if this is a good solution but I thought we could be a bit "lazy" and avoid unnecessary recursion which may or may not help depending on the size of your expression tree.

In the following expression there is no need to evaluate both A & B since C is already true:

{OR: [{/* ... */}, {/* ... */}, true]}
//    ^            ^            ^
//    A            B            C

Similarly there is no need to evaluate both A & B since C is already false:

{AND: [{/* ... */}, {/* ... */}, false]}
//     ^            ^            ^
//     A            B            C

With that in mind I came up with the following code:

const lazy_or = exp => exp.find(e => e === true);
const lazy_and = exp => exp.find(e => e === false);

const evaluate =
  exp =>
      typeof exp === "boolean"                ? exp
    : exp.OR && lazy_or(exp.OR)               ? true
    : exp.OR                                  ? exp.OR.some(evaluate)
    : exp.AND && lazy_and(exp.AND) === false  ? false
                                              : exp.AND.every(evaluate);

Upvotes: 2

adiga
adiga

Reputation: 35222

You could use a simple recursive function.

  • If the current object has OR key, then check if some of the items in the array are truthy.
  • If AND, check if every item is truthy.
  • If one of the item in the array is an object, recursively call the function on the object to get its value

const input={OR:[{AND:[false,true,true]},{OR:[true,false,false,{AND:[true,true]}]},true]};

function evaluate({ OR, AND }) {
  if (OR)
    return OR.some(c => typeof c === 'object' ? evaluate(c) : c)
  if (AND)
    return AND.every(c => typeof c === 'object' ? evaluate(c) : c)
}

console.log(evaluate(input))

Since the callback functions are same, you could also get the operation to a variable and call it dynamically:

function evaluate({ OR, AND }) {
  const array = OR ?? AND,
        operation = OR ? 'some' : 'every';
  
  return array[operation](c => typeof c === 'object' ? evaluate(c) : c)
}

OR

const evaluate = ({ OR, AND }) => OR ? OR.some(callback) : AND.every(callback),
      callback = c => typeof c === 'object' ? evaluate(c) : c

Upvotes: 7

VLAZ
VLAZ

Reputation: 28998

This is fundamentally the same operation as adiga's answer but variation uses mutual recursion between evaluate and the operator functions. The main benefit is separation between the algorithm traversing the tree evaluate and the actual operators used. It's easier to refactor to handle other operations if needed, by just adding them to operators.

const operators = {
  "OR" : arr => arr.some(evaluate),
  "AND": arr => arr.every(evaluate),
}

function evaluate(input) {
  if (typeof input !== "object") 
    return input;
  
  const [op, value] = Object.entries(input)[0];
  return operators[op](value);
}

test(true);
test(false);
test({"OR": [false, true]});
test({"OR": [false, false]});

test({"AND": [true, true]});
test({"AND": [false, true]});

test({
  'OR': [
      {
        'AND': [
            false, true, true
        ]
      },
      {
        'OR': [
            true, false, false, {
                'AND': [true, true]
            }
        ]
      },
      true
  ]
})

function test(input) {
  console.log(`evaluating: ${JSON.stringify(input, undefined, 2)}
  result: ${evaluate(input)}`)
}

To showcase the ease of extension, we can convert this to also handle mathematical operations by just adding operators that handle addition and multiplication:

const operators = {
  "OR" : arr => arr.some(evaluate),
  "AND": arr => arr.every(evaluate),
  "+": arr => arr.reduce((a, b) => a + evaluate(b), 0),
  "*": arr => arr.reduce((a, b) => a * evaluate(b), 1),
}

function evaluate(input) {
  if (typeof input !== "object") 
    return input;
  
  const [op, value] = Object.entries(input)[0];
  return operators[op](value);
}

test({"+": [2, 3]});
test({"*": [2, 3]});
test({
  '+': [
      {
        '*': [
            2, 2, 5
        ]
      },
      {
        '+': [
            6, 4, 3, {
                '*': [1, 7]
            }
        ]
      },
      2
  ]
})

function test(input) {
  console.log(`evaluating: ${JSON.stringify(input, undefined, 2)}
  result: ${evaluate(input)}`)
}

Upvotes: 1

t348575
t348575

Reputation: 753

function evalOBJ(obj) {
    let result = true;
    if (obj.OR) {
        result = false;
        for (const v of obj.OR) {
            if (typeof v === 'object') {
                result = result || evalOBJ(v);
            } else {
                result = result || v;
            }
        }
    } else if (obj.AND) {
        for (const v of obj.AND) {
            if (typeof v === 'object') {
                result = result && evalOBJ(v);
            } else {
                result = result && v;
            }
        }
    }
    return result;
}

Upvotes: 1

Related Questions