4

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.

2
  • safe way ... Where does it run? Client or server? Commented Aug 18, 2018 at 12:01
  • @JonasWilms runs web workers and node.js Commented Aug 18, 2018 at 12:34

2 Answers 2

2

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, '')

Sign up to request clarification or add additional context in comments.

2 Comments

Doesn't seem to work if I make the expression more complex: var string = "(5 ∈ y || 3 ∉ y) && (3 ∈ y || 7 ∉ y)";
@Harry That's because \S+ matches the parentheses too. If you have only numbers and letters, you should change the regex to: /(\w+)\s*∈\s*(\w+)/g
1

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!

5 Comments

Does this respect operator precedence? Also by looking at some of OP's examples, it will be quite hard to evaluate them with a custom parser.
@ibrahimmahrir Mathematical expression parsers are fairly simple, even if you don't use any of the libraries available (which make it almost trivial). I mean, it's something you do in college as a small part of a single class.
@ibrahim yes it will (mostly), and yes a custom parser is complicated but a lot of regexes will get complicated too...
I've just noticed: the splitting by the operator causes problems if there is more than one instance of the operator: "5 == 6 || 5 == 2 || 6 == 6" will evaluate to false.
@ibrahim yup, fixed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.