Harry
Harry

Reputation: 55029

How to evaluate set inclusion tests using JavaScript?

I want to eval expressions to check for truthiness.

The user is giving me expressions in the form of

x == y || a == (c + 3)

or

y >= 4

I would just use eval with:

expression.replace(/[^|()\d\+-><=!&]/g, '');
eval(expression);

Now the user wants to give me set inclusion expressions like:

5 ∈ y || 3 ∉ y

Where y is an array. What's the best safe way to handle that?

Edit: Thanks for your help, reading over the answers it's hard to decide which one to pick / which one is better.

Upvotes: 4

Views: 371

Answers (2)

Jonas Wilms
Jonas Wilms

Reputation: 138557

Its actually quite hard to replace a E b with b.includes(a) as you have to take care of operator predescendence. Therefore the only way seems to be to implement your own parser:

 const diad = (char, operation) => input =>  {
   // If the operator doesnt exist, don't evaluate
   if(!input.includes(char))
     return input;
   return input.split(char)
     .map(evaluate) // deep first
     .reduce(operation); // left to right
 };

 const bracketsFirst = input => {
   const opening = input.lastIndexOf("("); // the most inner one
   const closing = input.indexOf(")", opening);

   if(opening === -1 || closing === -1) // no brackets, don't evaluate here
     return input;

   const before = input.slice(0, opening);
   const after = input.slice(closing + 1);
   const middle = input.slice(opening + 1, closing);

   return before + evaluate(middle) + after; // just evaluate the thing in the brackets 
};

 let context = {};
 const evaluate = input => [
   bracketsFirst,
   // Boolean algebra
   diad("||", (a, b) => a || b), // lowest operator predescendence
   diad("&&", (a, b) => a && b),
   // Comparison:
   diad("==", (a, b) => a === b),
   diad("!=", (a, b) => a != b),
   diad("<=", (a, b) => a <= b),
   diad(">=", (a, b) => a >= b),
   diad("<", (a, b) => a < b),
   diad(">", (a, b) => a > b),
   // Math:
   diad("+", (a, b) => a + b),
   diad("-", (a, b) => a - b),
   diad("*", (a, b) => a * b),
   diad("/", (a, b) => a / b),
   // The custom operator:
   diad("E", (a, b) => b.includes(a)),
   // Booleans
   a => a.trim() === "true" ? true : a,
   a => a.trim() === "false" ? false : a,
   // Number literals & Identifiers
   a => +a || context[a.trim()] || a,
   a => { throw Error("Unknown:" + a) }
 ].reduce((out, fn) => typeof out === "string" ? fn(out) : out, input);

So you can do:

 context.a = [1,2,3];
 context.one = 1;
 evaluate("1 E a || 5 E a"); // false
 evaluate("one == 1(5 - 5) - 9"); // true :)

Try it!

Upvotes: 1

ibrahim mahrir
ibrahim mahrir

Reputation: 31712

Just check for the inclusion yourself without eval, replace the inclusion tests with either true or false. To achieve that you should have access to the sets by their name, you can group them into an object sets where the keys are names and the values are the sets themselves (see example bellow):

Inclusion test:

var newString = string.replace(/(\S+)\s*∈\s*(\S+)/g, function(match, item, set) {
    var theActualSet = ...;                  // get the actual set using 'set'
    return theActualSet.includes(+item);
});

Exclusion test:

var newString = string.replace(/(\S+)\s*∉\s*(\S+)/g, function(match, item, set) {
    var theActualSet = ...;                  // get the actual set using 'set'
    return !theActualSet.includes(+item);
});

Example:

var sets = {
  y: [1, 2, 3, 4, 5]
};

var string = "5 ∈ y || 3 ∉ y";

var newString = string.replace(/(\S+)\s*∈\s*(\S+)/g, function(match, item, set) {
    var theActualSet = sets[set];
    return theActualSet && theActualSet.includes(+item);
});

newString = newString.replace(/(\S+)\s*∉\s*(\S+)/g, function(match, item, set) {
    var theActualSet = sets[set];
    return theActualSet && !theActualSet.includes(+item);
});

console.log("The original string: '" + string + "'");
console.log("The result string: '" + newString + "'");
console.log("The result: " + eval(newString));

Now that will be easy to evaluate without any concerns, just remove anything other than |()\d\+-><=!& and the literals true and false and use eval to get the result of the evaluation.

Note: You can use 1 and 0 instead of true and false to make the removal of the unwanted characters easier: replace(/[^|()\d\+-><=!&]/g, '')

Upvotes: 2

Related Questions