Extend a questionnaire condition evaluator with numeric questions and nested AND/OR logic
Company: ZipHQ
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: medium
Interview Round: Technical Screen
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`.
```python
# 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:
```json
{
"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.
```hint 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 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.
```
#### 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.
```hint 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 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.
```
#### 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
### What a Strong Answer Covers
- Input formats and semantics pinned down and stated before coding
- Extension of the existing structure rather than a rewrite, with existing behavior preserved
- Type-aware parsing, and validation of the definition when it is loaded, with clear errors
- A condition model in which groups compose the Part 1 evaluators to any depth
- Tests for boundaries, invalid input, unanswered questions and nesting
### 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?
Overview: 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.
Read the full ZipHQ Software Engineer interview experience this question came from