So I have this S-expression array
const condition = [
'OR',
[
'AND',
['==', '$State', 'Alabama'],
['==', '$Profession', 'Software development']
],
['==', '$Undefined', ''],
[
'AND',
['==', '$State', 'Texas']
],
[
'OR',
[
'OR',
['==', '$Profession', 'Tradesperson']
]
]
]
I have this test case and function that need to be satisfied
const testCases = [
[{'State': 'Alabama', 'Profession': 'Software development'}, true],
[{'State': 'Texas'}, true],
[{'State': 'Alabama', 'Profession': 'Gaming'}, false],
[{'State': 'Utah'}, false],
[{'Profession': 'Town crier'}, false],
[{'Profession': 'Tradesperson'}, true],
[{}, false]
]
for (const [index, [context, expected]] of testCases.entries()) {
console.log(
evaluate(condition as Condition, context) === expected
? `${index} ok`
: `${index} FAIL`
)
}
What I have so far is
function evaluate (condition: Condition, context: Context): boolean {
if (isLogicalCondition(condition)) {
const [operator, ...conditions] = condition
}
if (isComparisonCondition(condition)) {
const [operator, variable, value] = condition
}
return false
}
The functions, types and variables are
type Context = {
[k: string]: string | undefined
}
enum LogicalOperator {
And = 'AND',
Or = 'OR'
}
enum ComparisonOperator {
Eq = '=='
}
type Operator = LogicalOperator | ComparisonOperator
type LogicalCondition = [LogicalOperator, ...Array<Condition>]
type Variable = string
type Value = string
type ComparisonCondition = [ComparisonOperator, Variable, Value]
type Condition = LogicalCondition | ComparisonCondition
function isLogicalCondition (condition: Condition): condition is LogicalCondition {
return Object.values(LogicalOperator).includes(condition[0] as LogicalOperator)
}
function isComparisonCondition (condition: Condition): condition is ComparisonCondition {
return Object.values(ComparisonOperator).includes(condition[0] as ComparisonOperator)
}
My question is how do I even think about this in an abstract enough way to solve this issue to satisfy the test without hardcoding results against the test? I am totally lost...
You should use recursion. The "OR" operator translates to a some
method call, and "AND" to a every
method call.
For equality, you should deal with any "$" prefix, in which case you have to look up the value in the context object. It would be nice to not assume that the first argument always has the "$"..., so I propose to use a mapper on both arguments, each time dealing with the potential "$".
You could use this function:
function evaluate(condition, context) {
let [operator, ...arguments] = condition;
if (operator === "==") {
return arguments.map(arg => arg[0] === "$" ? context[arg.slice(1)] : arg)
.reduce(Object.is);
}
if (operator === "OR") {
return arguments.some(argument => evaluate(argument, context));
}
if (operator === "AND") {
return arguments.every(argument => evaluate(argument, context));
}
throw "unknown operator " + operator;
}
Running the test cases:
function evaluate(condition, context) {
let [operator, ...arguments] = condition;
if (operator === "==") {
return arguments.map(arg => arg[0] === "$" ? context[arg.slice(1)] : arg)
.reduce(Object.is);
}
if (operator === "OR") {
return arguments.some(argument => evaluate(argument, context));
}
if (operator === "AND") {
return arguments.every(argument => evaluate(argument, context));
}
throw "unknown operator " + operator;
}
const condition = [
'OR',
[
'AND',
['==', '$State', 'Alabama'],
['==', '$Profession', 'Software development']
],
['==', '$Undefined', ''],
[
'AND',
['==', '$State', 'Texas']
],
[
'OR',
[
'OR',
['==', '$Profession', 'Tradesperson']
]
]
]
const testCases = [
[{'State': 'Alabama', 'Profession': 'Software development'}, true],
[{'State': 'Texas'}, true],
[{'State': 'Alabama', 'Profession': 'Gaming'}, false],
[{'State': 'Utah'}, false],
[{'Profession': 'Town crier'}, false],
[{'Profession': 'Tradesperson'}, true],
[{}, false]
]
for (const [index, [context, expected]] of testCases.entries()) {
console.log(
evaluate(condition, context) === expected
? `${index} ok`
: `${index} FAIL`
)
}