Skip to content

Commit 9341dbc

Browse files
authored
Merge pull request #187 from ikostan/exercism-sync/d34efc826287fdf7
[Sync Iteration] python/wordy/7
2 parents bc679b4 + 6029d7d commit 9341dbc

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed

solutions/python/wordy/7/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+
new_question: list[str] = _reformat(question)
46+
# Reduce iteratively: evaluate the first three-token slice
47+
# and fold the result left-to-right.
48+
while new_question:
49+
try:
50+
if len(new_question) == 3:
51+
_validate_evaluation_pattern(new_question)
52+
return _math_operation(new_question)
53+
if len(new_question) == 1:
54+
return int(new_question[0])
55+
_validate_evaluation_pattern(new_question[:3])
56+
result = _math_operation(new_question[:3])
57+
new_question = [str(result)] + new_question[3:]
58+
except Exception as exc:
59+
raise ValueError("syntax error") from exc
60+
raise ValueError("syntax error") # Safety net for empty cases
61+
62+
63+
def _math_operation(question: list[str]) -> int:
64+
"""
65+
Compute a single binary arithmetic operation.
66+
67+
Expects a three-token slice like ``['3', '+', '4']`` and returns
68+
the integer result. Division performs floor division (``//``) to
69+
match exercise rules.
70+
71+
:param question: Three tokens ``[lhs, operator, rhs]``.
72+
:type question: list[str]
73+
:returns: The computed integer result.
74+
:rtype: int
75+
"""
76+
math_operator: str = question[1]
77+
78+
if math_operator == "+":
79+
return int(question[0]) + int(question[-1])
80+
81+
if math_operator == "-":
82+
return int(question[0]) - int(question[-1])
83+
84+
if math_operator == "/":
85+
return int(question[0]) // int(question[-1])
86+
87+
if math_operator == "*":
88+
return int(question[0]) * int(question[-1])
89+
90+
raise ValueError("syntax error")
91+
92+
93+
def _validate_evaluation_pattern(val: list[str]) -> 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[str]
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[str]:
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[1:].isdigit()
128+
or item in STR_TO_OPERATOR
129+
or item 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)