|
| 1 | +""" |
| 2 | +Parse and evaluate simple math word problems |
| 3 | +returning the answer as an integer. |
| 4 | +
|
| 5 | +Handle a set of operations, in sequence. |
| 6 | +
|
| 7 | +Since these are verbal word problems, evaluate |
| 8 | +the expression from left-to-right, ignoring the |
| 9 | +typical order of operations. |
| 10 | +""" |
| 11 | + |
| 12 | +STR_TO_OPERATOR: dict[str, str] = { |
| 13 | + "plus": "+", |
| 14 | + "minus": "-", |
| 15 | + "multiplied": "*", |
| 16 | + "divided": "/", |
| 17 | +} |
| 18 | + |
| 19 | +REPLACEABLE: tuple = ("What", "is", "by") |
| 20 | + |
| 21 | + |
| 22 | +def answer(question: str) -> int: |
| 23 | + """ |
| 24 | + Evaluate a simple word-based arithmetic question from left to right. |
| 25 | +
|
| 26 | + :param question: The input question, e.g., "What is 5 plus 13?" |
| 27 | + :type question: str |
| 28 | + :returns: The evaluated integer result. |
| 29 | + :rtype: int |
| 30 | + :raises ValueError: If the operation is unknown or the syntax is invalid. |
| 31 | + """ |
| 32 | + new_question: list[str] = [ |
| 33 | + word |
| 34 | + for word in question.replace("?", "").split() |
| 35 | + if word not in REPLACEABLE |
| 36 | + ] |
| 37 | + # Reduce iteratively: |
| 38 | + # evaluate the first three-token slice and fold the result left-to-right. |
| 39 | + try: |
| 40 | + result, new_question = int(new_question[0]), new_question[1:] |
| 41 | + while new_question: |
| 42 | + result, new_question = _math_operation(result, new_question) |
| 43 | + except ValueError as exc: |
| 44 | + if exc.args[0] == "unknown operation": |
| 45 | + raise exc |
| 46 | + raise ValueError("syntax error") from exc |
| 47 | + except IndexError as exc: |
| 48 | + raise ValueError("syntax error") from exc |
| 49 | + |
| 50 | + return result |
| 51 | + |
| 52 | + |
| 53 | +def _math_operation(result: int, question: list[str]) -> tuple[int, list[str]]: |
| 54 | + """ |
| 55 | + Compute a single binary arithmetic step for the current operator. |
| 56 | +
|
| 57 | + Expects the next tokens of the question to begin with the operator word |
| 58 | + (e.g. ``plus``, ``minus``, ``multiplied``, ``divided``) followed by the |
| 59 | + right-hand side operand. Division uses floor division (``//``) to comply |
| 60 | + with the exercise rules. |
| 61 | +
|
| 62 | + :param result: Accumulated left-hand value computed so far. |
| 63 | + :type result: int |
| 64 | + :param question: Remaining tokens starting with the operator then rhs |
| 65 | + integer, e.g. ``['plus', '4', ...]``. |
| 66 | + :type question: list[str] |
| 67 | + :returns: A tuple of the new accumulated result and the remaining tokens |
| 68 | + after consuming the operator and rhs. |
| 69 | + :rtype: tuple[int, list[str]] |
| 70 | + :raises ValueError: If the operator is unknown or the token sequence is |
| 71 | + malformed. |
| 72 | + """ |
| 73 | + math_operator: str = question[0] |
| 74 | + # if the question contains an unknown operation. |
| 75 | + if ( |
| 76 | + math_operator not in STR_TO_OPERATOR |
| 77 | + and not math_operator.isdigit() |
| 78 | + ): |
| 79 | + raise ValueError("unknown operation") |
| 80 | + # if the question is malformed or invalid. |
| 81 | + if math_operator.isdigit(): |
| 82 | + raise ValueError("syntax error") |
| 83 | + # if the question is malformed or invalid. |
| 84 | + if math_operator in STR_TO_OPERATOR and len(question) == 1: |
| 85 | + raise ValueError("syntax error") |
| 86 | + |
| 87 | + if STR_TO_OPERATOR[math_operator] == "+": |
| 88 | + result += int(question[1]) |
| 89 | + |
| 90 | + if STR_TO_OPERATOR[math_operator] == "-": |
| 91 | + result -= int(question[1]) |
| 92 | + |
| 93 | + if STR_TO_OPERATOR[math_operator] == "/": |
| 94 | + result //= int(question[1]) |
| 95 | + |
| 96 | + if STR_TO_OPERATOR[math_operator] == "*": |
| 97 | + result *= int(question[1]) |
| 98 | + |
| 99 | + return result, question[2:] |
0 commit comments