Skip to content

Commit d7385b7

Browse files
authored
Merge pull request #179 from ikostan/exercism-sync/383869105c757f18
[Sync Iteration] python/wordy/5
2 parents f9832bd + 81108da commit d7385b7

1 file changed

Lines changed: 159 additions & 0 deletions

File tree

solutions/python/wordy/5/wordy.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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 = {
13+
"plus": "+",
14+
"minus": "-",
15+
"multiplied": "*",
16+
"divided": "/",
17+
}
18+
19+
WRONG_OPERATORS: list[str] = [
20+
"plus?",
21+
"minus?",
22+
"multiplied?",
23+
"divided?",
24+
"plus plus",
25+
"plus multiplied",
26+
"minus multiplied",
27+
"minus minus",
28+
"multiplied multiplied",
29+
"divided divided",
30+
"What is?",
31+
]
32+
33+
34+
def answer(question: str) -> int:
35+
"""
36+
Evaluate a simple word-based arithmetic question from left to right.
37+
38+
:param question: The input question, e.g., "What is 5 plus 13?"
39+
:type question: str
40+
:returns: The evaluated integer result.
41+
:rtype: int
42+
:raises ValueError: If the operation is unknown or the syntax is invalid.
43+
"""
44+
_validate_errors(question)
45+
result: int = 0
46+
new_question: list[str] = _reformat(question)
47+
# Reduce iteratively: evaluate the first three-token slice
48+
# and fold the result left-to-right.
49+
while new_question:
50+
try:
51+
if len(new_question) == 3:
52+
_validate_evaluation_pattern(new_question)
53+
return _math_operation(new_question)
54+
55+
if len(new_question) == 1:
56+
return int(new_question[0])
57+
58+
_validate_evaluation_pattern(new_question[:3])
59+
result = _math_operation(new_question[:3])
60+
new_question = [str(result)] + new_question[3:]
61+
except Exception as exc:
62+
raise ValueError("syntax error") from exc
63+
64+
65+
def _math_operation(question: list[str]) -> int:
66+
"""
67+
Compute a single binary arithmetic operation.
68+
69+
Expects a three-token slice like ``['3', '+', '4']`` and returns
70+
the integer result. Division performs floor division (``//``) to
71+
match exercise rules.
72+
73+
:param question: Three tokens ``[lhs, operator, rhs]``.
74+
:type question: list[str]
75+
:returns: The computed integer result.
76+
:rtype: int
77+
"""
78+
math_operator: str = question[1]
79+
result: int = 0
80+
81+
if math_operator == "+":
82+
result = int(question[0]) + int(question[-1])
83+
elif math_operator == "-":
84+
result = int(question[0]) - int(question[-1])
85+
elif math_operator == "/":
86+
result = int(question[0]) // int(question[-1])
87+
elif math_operator == "*":
88+
result = int(question[0]) * int(question[-1])
89+
90+
return result
91+
92+
93+
def _validate_evaluation_pattern(val: list) -> None:
94+
"""
95+
Ensure a token slice matches expected evaluation patterns.
96+
97+
:param val: Token slice to validate, e.g.,
98+
['3', '+', '4'] or ['+', '4'] during reduction.
99+
:type val: list
100+
:raises ValueError: If the pattern is invalid (syntax error).
101+
"""
102+
if len(val) == 3 and val[1] not in STR_TO_OPERATOR.values():
103+
raise ValueError("syntax error")
104+
105+
if len(val) == 2 and val[0] not in STR_TO_OPERATOR.values():
106+
raise ValueError("syntax error")
107+
108+
109+
def _reformat(question: str) -> list:
110+
"""
111+
Tokenize a natural-language math question into numbers
112+
and operator symbols.
113+
114+
:param question: Raw question string.
115+
:type question: str
116+
:returns: Token list with numbers and operator symbols.
117+
:rtype: list[str]
118+
"""
119+
# 1: Remove '?' mark
120+
question = question.replace("?", "")
121+
# 2: Convert all operators writen in word into proper math sign
122+
question_list: list[str] = question.split()
123+
formated_question_list: list[str] = []
124+
for item in question_list:
125+
if not (
126+
item.isdigit()
127+
or item in STR_TO_OPERATOR
128+
or item in STR_TO_OPERATOR.values()
129+
or any(val in item for val in STR_TO_OPERATOR.values())
130+
):
131+
continue
132+
133+
if item in STR_TO_OPERATOR:
134+
formated_question_list.append(STR_TO_OPERATOR[item])
135+
elif item.isdigit():
136+
formated_question_list.append(item)
137+
elif item in STR_TO_OPERATOR.values():
138+
formated_question_list.append(item)
139+
elif any(val in item for val in STR_TO_OPERATOR.values()):
140+
formated_question_list.append(item)
141+
142+
return formated_question_list
143+
144+
145+
def _validate_errors(question: str) -> None:
146+
"""
147+
Pre-validate unsupported or malformed questions.
148+
149+
:param question: Raw question string.
150+
:type question: str
151+
:raises ValueError: Unknown operation or malformed
152+
phrasing (syntax error).
153+
"""
154+
if "cubed" in question:
155+
raise ValueError("unknown operation")
156+
157+
for item in WRONG_OPERATORS:
158+
if item in question:
159+
raise ValueError("syntax error")

0 commit comments

Comments
 (0)