Reputation: 3240
Considering the following JSON
example of logic, where when you see the value
prop it is deriving its value from user input and comparing that against the expression:
{
"truthy": [
[
"AND",
{
"operator": "=",
"compareType": "text",
"value": "fname",
"expression": "John"
},
{
"operator": "=",
"compareType": "text",
"value": "lname",
"expression": "Doe"
}
],
"OR",
{
"operator": "=",
"compareType": "boolean",
"value": "Agree",
"expression": true
},
"OR",
[
"AND",
{
"operator": "=",
"compareType": "text",
"value": "fname",
"expression": "Jane"
},
{
"operator": "=",
"compareType": "text",
"value": "lname",
"expression": "Doe"
}
]
]
}
The translated expression would be:
(fname === "John" && lname === "Doe") || agree === true || (fname === "Jane" && lname === "Doe")
Right now what I'm doing is iterating over the structure and pushing values to an array, and later using eval
to return true
or false
.
for (i; i < v.length; i++) {
if (Array.isArray(v[i])) {
e.push("(true===");
e.push(this.parseBlocks(v[i], a, 0));
e.push(")");
} else if (v[i] === "OR") {
e.push("||");
} else if (v[i] === "AND") {
e.push("&&");
} else {
e.push(true);
e.push("===");
e.push(this.parseBlocks([v[i]], a, 0));
}
}
return eval(e.join(""));
This works great, but was wondering if there would be a more precise way of handling this without building an expression to pass into eval
?
The expression that the above code would generate if first name was John and last name was Doe and the checkbox (i.e. Agree) was not checked would be:
eval("(true === true) || true === false || (true === false)")
I didn't need to construct this string to evaluate when it was a simpler expression. For example, the two OR strings in between everything.
The code not included is the actual parsing function, but what it does is takes the first element in the array (i.e. "AND", "OR") and uses that to determine if that particular block of logic returns true or false.
How could this be handled with the consideration of potential deeper nesting of logic, to determine the outcome without using eval, or considering I'm controlling what's being passed into eval perhaps it may provide better performance in comparison?
Requirements:
Inspiration:
Upvotes: 3
Views: 269
Reputation: 6702
IMO, you need two functions:
evaluate
leafValue
leafValue
This one is the most straightforward. You just need a set of data to evaluate from. Here I created an object called data
but in your question you seem to want to get the values from current scope (fname
as opposed to someObject.fname
) so you could just as well replace my data
with this
.
I then implemented a simple switch / case
to potentially account for different operator
values. You would need to complicate this a little bit if you also want to implement more complex compareType
options, but luckily here, JS is pretty forgiving if we don't use the strict identity ===
.
In the end, most of the logic is here:
return data[item.value] === item.expression
evaluate
This one is a little trickier. I implemented a depth-first recursive function that will flatten the initial json object iteration by iteration.
Every item that is an Array
has to go through evaluate
first before it can be useful (depth-first and recursive)
in the order of the array, the first 3 items must always contain 1 combinator ("OR"
or "AND"
) and 2 values (a previous value calculated by evaluate
, or the result of an object passed through leafValue
). evaluate
finds which is which, and applies the logical operation.
in the array I'm currently evaluating, evaluate
replaces the first 3 items with the result of step 2.
Repeat from step 2. If there aren't enough items left, evaluate
returns the single value left in the array.
For neatness, I wrapped the both of them in a processData
function.
function processData(data, json) {
const combinators = ["OR", "AND"]
function leafValue(item) {
switch (item.operator) {
case '=':
return data[item.value] === item.expression
}
throw new Error(`unknown comparison operator ${item.operator}`)
}
function evaluate(json) {
const evaluated = json.map(item => Array.isArray(item) ? evaluate(item) : item)
while (evaluated.length >= 3) {
const chunk = [evaluated[0], evaluated[1], evaluated[2]]
const combinator = chunk.find(item => combinators.includes(item))
const [A, B] = chunk.filter(item => !combinators.includes(item))
const valueA = typeof A === 'boolean' ? A : leafValue(A)
const valueB = typeof B === 'boolean' ? B : leafValue(B)
const result = combinator === 'OR' ?
(valueA || valueB) :
(valueA && valueB)
evaluated.splice(0, 3, result)
}
return evaluated[0]
}
return evaluate(json)
}
const json = {
"truthy": [
[
"AND",
{
"operator": "=",
"compareType": "text",
"value": "fname",
"expression": "John"
},
{
"operator": "=",
"compareType": "text",
"value": "lname",
"expression": "Doe"
}
],
"OR",
{
"operator": "=",
"compareType": "boolean",
"value": "Agree",
"expression": true
},
"OR", [
"AND",
{
"operator": "=",
"compareType": "text",
"value": "fname",
"expression": "Jane"
},
{
"operator": "=",
"compareType": "text",
"value": "lname",
"expression": "Doe"
}
]
]
}
const data = {
fname: 'John',
lname: 'Doe',
}
const result = processData(data, json.truthy)
console.log(result)
Upvotes: 1