Katkat

Extend a questionnaire condition evaluator with numeric questions and nested AND/OR logic

ZipHQSoftware Engineer · Technical Screen

Last updated

A backend practical exercise that extends an existing Python questionnaire parser and condition evaluator. Part one adds numeric questions with numeric comparison conditions; part two adds AND/OR conditions across different questions, including nested groups. Both parts require new automated tests.

You are working in an existing Python project that parses questionnaire definitions and evaluates conditions on them. A questionnaire is an ordered list of questions. A question can carry a condition on an earlier question's answer, and it is shown only when its condition holds. Today the project supports only free-text questions, and conditions that compare a text answer with a fixed value.

Add two features to the existing code, and write new test cases for each. You may look up documentation, but you may not use an AI assistant.

The original repository is not available. The starter below is a minimal stand-in with the same responsibilities (parse a questionnaire definition, parse answers, evaluate conditions, list the visible questions). Treat it as the existing code you are extending. answers maps a question ID to the value returned by parse_answer.

# questionnaire.py (existing code)
from dataclasses import dataclass
from typing import Optional

@dataclass
class Condition:
 question_id: str # the earlier question whose answer is checked
 operator: str # "equals" or "not_equals"
 value: str

@dataclass
class Question:
 id: str
 prompt: str
 type: str # only "text" today
 condition: Optional[Condition] = None

def parse_questionnaire(spec: dict) -> list[Question]:
 questions = []
 for q in spec["questions"]:
 condition = None
 if "condition" in q:
 c = q["condition"]
 condition = Condition(c["question_id"], c["operator"], c["value"])
 questions.append(Question(q["id"], q["prompt"], q["type"], condition))
 return questions

def parse_answer(question: Question, raw: str) -> str:
 if question.type == "text":
 return raw.strip()
 raise ValueError(f"unsupported question type: {question.type}")

def evaluate(condition: Condition, answers: dict[str, str]) -> bool:
 answer = answers.get(condition.question_id)
 if answer is None: # the referenced question is unanswered
 return False
 if condition.operator == "equals":
 return answer == condition.value
 if condition.operator == "not_equals":
 return answer != condition.value
 raise ValueError(f"unsupported operator: {condition.operator}")

def visible_questions(questions: list[Question], answers: dict[str, str]) -> list[str]:
 return [q.id for q in questions
 if q.condition is None or evaluate(q.condition, answers)]

Questionnaire definitions arrive as parsed JSON, for example:

{
 "questions": [
 {"id": "request_type", "type": "text", "prompt": "What are you requesting?"},
 {"id": "justification", "type": "text", "prompt": "Why is it needed?",
 "condition": {"question_id": "request_type", "operator": "equals", "value": "software"}}
 ]
}

With the answer software for request_type, visible_questions returns ["request_type", "justification"]; with hardware it returns ["request_type"].

Constraints and Clarifications

  • Extend the existing code. Questionnaires and tests that use only text questions must keep working unchanged.
  • No example inputs or outputs were provided for the new features. Deciding the definition format and the expected results is part of the task, so state your choices before you write code.
  • Each part needs new automated tests.

Clarifying Questions

  • Should numeric answers accept decimals and negative numbers? Are inputs such as 1,000, 1e3 or 42 valid?
  • Which comparison operators do numeric conditions need besides equals and not equals?
  • Should an answer that is not a valid number be rejected when it is entered, or stored and treated as not matching?
  • May a condition refer to any question, or only to questions earlier in the questionnaire?
  • In Part 2, may one group mix AND and OR, and how deeply can groups nest?
  • If a question is hidden, should its previously entered answer still count in later conditions?

Part 1 — Numeric questions and numeric conditions

Add a number question type. A raw numeric answer must be parsed and validated, and conditions on a number question must compare numerically, with ordering comparisons as well as equality. For example, a question should be able to appear only when the answer to a number question amount is greater than 10000. Keep text questions working exactly as before, and add test cases.

What This Part Should Cover

  • Parsing and validating numeric input, including invalid and edge-case strings
  • Numeric rather than lexical comparison, with a clearly chosen set of operators
  • Validation of each condition against the type of the question it references
  • Tests, including boundary values and unchanged text behavior

Part 2 — AND/OR conditions across questions

A question's condition must now be able to combine conditions on different questions with AND and OR, and combinations can nest. For example: show approver when (request_type equals software AND amount is greater than 10000) OR amount is at least 50000. Decide how such a condition is written in the questionnaire definition, keep every existing single condition valid, build the evaluation on top of the Part 1 evaluators, and add test cases.

What This Part Should Cover

  • A definition format for AND/OR groups that nests and stays backward compatible
  • An evaluator for groups built from the existing single-condition evaluators
  • Handling of malformed groups, and of unanswered questions inside groups
  • Tests that exercise each branch of a nested example

Hints

Hint 1

Where text becomes a number: Decide at which point a raw answer turns into a number, and what a comparison would do if it were handed two strings instead.

Hint 2

Catch mistakes before users do: Some broken conditions, such as an ordering comparison on a text question, are visible in the questionnaire definition itself. Consider when they should be reported.

Hint 3

Write the example down first: Express the example condition by hand in your proposed format before writing any code. If the format needs precedence rules to be read, consider what happens one nesting level deeper.

Hint 4

Reuse the leaf: Your Part 1 code already decides a single comparison. Think about what a combination needs to know about its parts, and nothing more.

Follow-up questions

  • How would you add `not`, an `in` (one of several values) operator, or `between`, without changing the code that evaluates existing conditions?
  • An author makes a mistake deep inside a nested condition. How would your error message point to it?
  • With thousands of questions, how would you avoid re-evaluating every condition each time one answer changes?
  • How would you detect a condition that can never be true, such as `amount` greater than 10 AND `amount` less than 5?