Reputation: 55029
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
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 :)
Upvotes: 1
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