javascripts-expression

How to make your way through an S-expression tree to satisfy a test case


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...


Solution

  • 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`
      )
    }