j-js commited on
Commit
c13d1bd
·
verified ·
1 Parent(s): 9bd8108

Update solver_percent.py

Browse files
Files changed (1) hide show
  1. solver_percent.py +512 -105
solver_percent.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  import re
4
  from typing import Optional, List
5
 
@@ -10,180 +11,586 @@ def _nums(text: str) -> List[float]:
10
  return [float(x) for x in re.findall(r"-?\d+(?:\.\d+)?", text)]
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  def solve_percent(text: str) -> Optional[SolverResult]:
14
- lower = (text or "").lower()
 
15
 
16
- if "%" not in lower and "percent" not in lower:
 
 
 
 
 
 
17
  return None
18
 
19
  nums = _nums(lower)
20
 
21
- # Pattern 1: "40% of a number is 24"
 
 
22
  m = re.search(
23
- r"(\d+(?:\.\d+)?)\s*%?\s*(?:percent)?\s*of\s+(?:a\s+number|number|x|an?\s+amount).*?\bis\b\s*(-?\d+(?:\.\d+)?)",
24
- lower,
25
  )
26
  if m:
27
- pct = float(m.group(1)) / 100.0
28
- value = float(m.group(2))
29
- if pct == 0:
30
- return None
31
- result = value / pct
32
- return SolverResult(
33
- domain="quant",
34
- solved=True,
35
  topic="percent",
36
- answer_value=f"{result:g}",
37
- internal_answer=f"{result:g}",
38
- steps=[
 
39
  "Convert the percent to a decimal.",
40
- "Set up percent × whole = part.",
41
- "Divide the part by the decimal percent to get the whole.",
42
  ],
43
  )
44
 
45
- # Pattern 2: "What is 25% of 80?"
 
 
 
46
  m = re.search(
47
- r"what\s+is\s+(\d+(?:\.\d+)?)\s*%?\s*(?:percent)?\s*of\s*(-?\d+(?:\.\d+)?)",
48
- lower,
49
  )
50
  if m:
51
- pct = float(m.group(1)) / 100.0
52
- whole = float(m.group(2))
53
- result = pct * whole
54
- return SolverResult(
55
- domain="quant",
56
- solved=True,
 
57
  topic="percent",
58
- answer_value=f"{result:g}",
59
- internal_answer=f"{result:g}",
60
- steps=[
 
61
  "Convert the percent to a decimal.",
62
- "Multiply the whole by the decimal percent.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  ],
64
  )
65
 
66
- # Pattern 3: "24 is what percent of 60?"
 
 
67
  m = re.search(
68
- r"(-?\d+(?:\.\d+)?)\s+is\s+what\s+percent\s+of\s+(-?\d+(?:\.\d+)?)",
69
- lower,
70
  )
71
  if m:
72
  part = float(m.group(1))
73
  whole = float(m.group(2))
74
  if whole == 0:
75
- return None
76
- result = (part / whole) * 100
77
- return SolverResult(
78
- domain="quant",
79
- solved=True,
80
  topic="percent",
81
- answer_value=f"{result:g}",
82
- internal_answer=f"{result:g}",
83
- steps=[
 
84
  "Use percent = part ÷ whole × 100.",
85
  "Divide the part by the whole.",
86
- "Convert the decimal to a percent.",
87
  ],
88
  )
89
 
90
- # Pattern 4: "What percent of 60 is 24?"
91
  m = re.search(
92
- r"what\s+percent\s+of\s+(-?\d+(?:\.\d+)?)\s+is\s+(-?\d+(?:\.\d+)?)",
93
- lower,
94
  )
95
  if m:
96
  whole = float(m.group(1))
97
  part = float(m.group(2))
98
  if whole == 0:
99
- return None
100
- result = (part / whole) * 100
101
- return SolverResult(
102
- domain="quant",
103
- solved=True,
104
  topic="percent",
105
- answer_value=f"{result:g}",
106
- internal_answer=f"{result:g}",
107
- steps=[
 
108
  "Use percent = part ÷ whole × 100.",
109
  "Divide the part by the whole.",
110
- "Convert to percent form.",
111
  ],
112
  )
113
 
114
- # Pattern 5: percent increase/decrease
 
 
115
  m = re.search(
116
- r"(-?\d+(?:\.\d+)?)\s*%?\s*(?:percent)?\s*(increase|decrease)[sd]?\s+from\s+(-?\d+(?:\.\d+)?)\s+to\s+(-?\d+(?:\.\d+)?)",
117
- lower,
118
  )
119
  if m:
120
- # rare wording, not ideal
121
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  m = re.search(
124
- r"(?:increase|decrease)[sd]?\s+from\s+(-?\d+(?:\.\d+)?)\s+to\s+(-?\d+(?:\.\d+)?)",
125
- lower,
126
  )
127
  if m and ("percent" in lower or "%" in lower):
128
  old = float(m.group(1))
129
  new = float(m.group(2))
130
  if old == 0:
131
- return None
132
- result = ((new - old) / old) * 100
133
- return SolverResult(
134
- domain="quant",
135
- solved=True,
136
  topic="percent_change",
137
- answer_value=f"{result:g}",
138
- internal_answer=f"{result:g}",
139
- steps=[
140
- "Find the change: new − old.",
141
- "Divide by the original value.",
142
- "Multiply by 100 to convert to a percent.",
 
143
  ],
144
  )
145
 
146
- # Pattern 6: "increased by 20%" / "decreased by 20%"
 
 
147
  m = re.search(
148
- r"(-?\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased)\s+by\s+(\d+(?:\.\d+)?)\s*%",
149
- lower,
150
  )
151
  if m:
152
- value = float(m.group(1))
153
  direction = m.group(2)
154
- pct = float(m.group(3)) / 100.0
155
- result = value * (1 + pct if direction == "increased" else 1 - pct)
156
- return SolverResult(
157
- domain="quant",
158
- solved=True,
159
  topic="percent_change",
160
- answer_value=f"{result:g}",
161
- internal_answer=f"{result:g}",
162
- steps=[
 
163
  "Convert the percent to a decimal.",
164
- "Use multiplier 1 + p for increase or 1 − p for decrease.",
165
- "Multiply the original value by that multiplier.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  ],
167
  )
168
 
169
- # Pattern 7: percent error / generic part-whole if 2 nums and clue words
170
- if len(nums) >= 2 and ("of" in lower or "is" in lower):
171
- # conservative fallback
172
- pct = nums[0]
173
- whole = nums[1]
174
- if pct <= 100:
175
- result = (pct / 100.0) * whole
176
- return SolverResult(
177
- domain="quant",
178
- solved=True,
179
- topic="percent",
180
- answer_value=f"{result:g}",
181
- internal_answer=f"{result:g}",
182
- steps=[
183
- "Interpret the first number as a percent.",
184
- "Convert it to a decimal.",
185
- "Multiply by the base quantity.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  ],
187
  )
188
 
189
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ import math
4
  import re
5
  from typing import Optional, List
6
 
 
11
  return [float(x) for x in re.findall(r"-?\d+(?:\.\d+)?", text)]
12
 
13
 
14
+ def _clean(text: str) -> str:
15
+ return re.sub(r"\s+", " ", (text or "").strip()).lower()
16
+
17
+
18
+ def _pct_to_decimal(p: float) -> float:
19
+ return p / 100.0
20
+
21
+
22
+ def _safe_fmt(x: float) -> str:
23
+ return f"{x:g}"
24
+
25
+
26
+ def _build_result(
27
+ *,
28
+ topic: str,
29
+ internal_answer: Optional[float] = None,
30
+ ask: str,
31
+ given: str,
32
+ method: List[str],
33
+ ) -> SolverResult:
34
+ return SolverResult(
35
+ domain="quant",
36
+ solved=True,
37
+ topic=topic,
38
+ answer_value=None, # never expose final answer
39
+ internal_answer=_safe_fmt(internal_answer) if internal_answer is not None else None,
40
+ steps=[
41
+ f"What the question is asking: {ask}",
42
+ f"What is given: {given}",
43
+ *method,
44
+ ],
45
+ )
46
+
47
+
48
+ def _fallback_percent() -> SolverResult:
49
+ return SolverResult(
50
+ domain="quant",
51
+ solved=False,
52
+ topic="percent",
53
+ answer_value=None,
54
+ internal_answer=None,
55
+ steps=[
56
+ "What the question is asking: identify whether you need the part, the whole, the percent, or the percent change.",
57
+ "Key relationships:",
58
+ "• part = percent × whole",
59
+ "• percent = part ÷ whole × 100",
60
+ "• percent change = change ÷ original × 100",
61
+ "Start by labeling the numbers in the problem as original, new, part, whole, rate, or time.",
62
+ ],
63
+ )
64
+
65
+
66
  def solve_percent(text: str) -> Optional[SolverResult]:
67
+ raw = text or ""
68
+ lower = _clean(raw)
69
 
70
+ percent_words = [
71
+ "%", "percent", "percentage", "pct",
72
+ "increase", "decrease", "discount", "markup",
73
+ "profit", "loss", "interest", "percentile",
74
+ "tax", "tip", "sale"
75
+ ]
76
+ if not any(w in lower for w in percent_words):
77
  return None
78
 
79
  nums = _nums(lower)
80
 
81
+ # --------------------------------------------------
82
+ # 1) BASIC PART: "What is 25% of 80?"
83
+ # --------------------------------------------------
84
  m = re.search(
85
+ r"what\s+is\s+(\d+(?:\.\d+)?)\s*(?:%|percent|pct)?\s+of\s+(-?\d+(?:\.\d+)?)",
86
+ lower
87
  )
88
  if m:
89
+ pct = float(m.group(1))
90
+ whole = float(m.group(2))
91
+ result = _pct_to_decimal(pct) * whole
92
+ return _build_result(
 
 
 
 
93
  topic="percent",
94
+ internal_answer=result,
95
+ ask="Find the part when the percent and whole are known.",
96
+ given=f"percent = {pct}%, whole = {whole}",
97
+ method=[
98
  "Convert the percent to a decimal.",
99
+ "Use part = percent × whole.",
100
+ "Multiply the decimal percent by the whole.",
101
  ],
102
  )
103
 
104
+ # --------------------------------------------------
105
+ # 2) BASIC WHOLE: "40% of a number is 24"
106
+ # Also handles "60 is 25% of what number?"
107
+ # --------------------------------------------------
108
  m = re.search(
109
+ r"(\d+(?:\.\d+)?)\s*(?:%|percent|pct)\s+of\s+(?:a\s+number|number|what\s+number|x|y|an?\s+amount|the\s+number).*?\bis\s+(-?\d+(?:\.\d+)?)",
110
+ lower
111
  )
112
  if m:
113
+ pct = float(m.group(1))
114
+ part = float(m.group(2))
115
+ dec = _pct_to_decimal(pct)
116
+ if dec == 0:
117
+ return _fallback_percent()
118
+ result = part / dec
119
+ return _build_result(
120
  topic="percent",
121
+ internal_answer=result,
122
+ ask="Find the whole when the percent and part are known.",
123
+ given=f"percent = {pct}%, part = {part}",
124
+ method=[
125
  "Convert the percent to a decimal.",
126
+ "Use part = percent × whole.",
127
+ "Rearrange to whole = part ÷ percent.",
128
+ ],
129
+ )
130
+
131
+ m = re.search(
132
+ r"(-?\d+(?:\.\d+)?)\s+is\s+(\d+(?:\.\d+)?)\s*(?:%|percent|pct)\s+of\s+(?:what|which)\s+(?:number|value|amount)",
133
+ lower
134
+ )
135
+ if m:
136
+ part = float(m.group(1))
137
+ pct = float(m.group(2))
138
+ dec = _pct_to_decimal(pct)
139
+ if dec == 0:
140
+ return _fallback_percent()
141
+ result = part / dec
142
+ return _build_result(
143
+ topic="percent",
144
+ internal_answer=result,
145
+ ask="Find the original whole from a known part and percent.",
146
+ given=f"part = {part}, percent = {pct}%",
147
+ method=[
148
+ "Convert the percent to decimal form.",
149
+ "Use part = percent × whole.",
150
+ "Solve for the whole by dividing the part by the decimal percent.",
151
  ],
152
  )
153
 
154
+ # --------------------------------------------------
155
+ # 3) WHAT PERCENT: "24 is what percent of 60?"
156
+ # --------------------------------------------------
157
  m = re.search(
158
+ r"(-?\d+(?:\.\d+)?)\s+is\s+what\s+(?:%|percent|percentage)\s+of\s+(-?\d+(?:\.\d+)?)",
159
+ lower
160
  )
161
  if m:
162
  part = float(m.group(1))
163
  whole = float(m.group(2))
164
  if whole == 0:
165
+ return _fallback_percent()
166
+ result = (part / whole) * 100.0
167
+ return _build_result(
 
 
168
  topic="percent",
169
+ internal_answer=result,
170
+ ask="Find what percent one value is of another.",
171
+ given=f"part = {part}, whole = {whole}",
172
+ method=[
173
  "Use percent = part ÷ whole × 100.",
174
  "Divide the part by the whole.",
175
+ "Convert the result to percent form.",
176
  ],
177
  )
178
 
 
179
  m = re.search(
180
+ r"what\s+(?:%|percent|percentage)\s+of\s+(-?\d+(?:\.\d+)?)\s+is\s+(-?\d+(?:\.\d+)?)",
181
+ lower
182
  )
183
  if m:
184
  whole = float(m.group(1))
185
  part = float(m.group(2))
186
  if whole == 0:
187
+ return _fallback_percent()
188
+ result = (part / whole) * 100.0
189
+ return _build_result(
 
 
190
  topic="percent",
191
+ internal_answer=result,
192
+ ask="Find the percent represented by the part out of the whole.",
193
+ given=f"whole = {whole}, part = {part}",
194
+ method=[
195
  "Use percent = part ÷ whole × 100.",
196
  "Divide the part by the whole.",
197
+ "Then convert to a percent.",
198
  ],
199
  )
200
 
201
+ # --------------------------------------------------
202
+ # 4) PERCENT CHANGE: "from 80 to 60"
203
+ # --------------------------------------------------
204
  m = re.search(
205
+ r"(?:what\s+(?:is\s+the\s+)?)?(?:percent\s+)?(increase|decrease|change).*?from\s+(-?\d+(?:\.\d+)?)\s+to\s+(-?\d+(?:\.\d+)?)",
206
+ lower
207
  )
208
  if m:
209
+ direction = m.group(1)
210
+ old = float(m.group(2))
211
+ new = float(m.group(3))
212
+ if old == 0:
213
+ return _fallback_percent()
214
+ result = ((new - old) / old) * 100.0
215
+ return _build_result(
216
+ topic="percent_change",
217
+ internal_answer=abs(result) if direction in ("increase", "decrease") else result,
218
+ ask="Find the percent change relative to the original value.",
219
+ given=f"original = {old}, new = {new}",
220
+ method=[
221
+ "Find the change: new − original.",
222
+ "Use the original value in the denominator.",
223
+ "Compute change ÷ original × 100.",
224
+ "Match the sign or wording to increase vs decrease.",
225
+ ],
226
+ )
227
 
228
  m = re.search(
229
+ r"from\s+(-?\d+(?:\.\d+)?)\s+to\s+(-?\d+(?:\.\d+)?)",
230
+ lower
231
  )
232
  if m and ("percent" in lower or "%" in lower):
233
  old = float(m.group(1))
234
  new = float(m.group(2))
235
  if old == 0:
236
+ return _fallback_percent()
237
+ result = ((new - old) / old) * 100.0
238
+ return _build_result(
 
 
239
  topic="percent_change",
240
+ internal_answer=result,
241
+ ask="Find the percent change between the starting value and ending value.",
242
+ given=f"original = {old}, new = {new}",
243
+ method=[
244
+ "Subtract to find the change.",
245
+ "Divide by the original value, not the new value.",
246
+ "Multiply by 100 to convert to percent form.",
247
  ],
248
  )
249
 
250
+ # --------------------------------------------------
251
+ # 5) NEW VALUE AFTER CHANGE: "80 increased by 25%"
252
+ # --------------------------------------------------
253
  m = re.search(
254
+ r"(-?\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased|reduced)\s+by\s+(\d+(?:\.\d+)?)\s*%",
255
+ lower
256
  )
257
  if m:
258
+ original = float(m.group(1))
259
  direction = m.group(2)
260
+ pct = float(m.group(3))
261
+ dec = _pct_to_decimal(pct)
262
+ multiplier = 1 + dec if direction == "increased" else 1 - dec
263
+ result = original * multiplier
264
+ return _build_result(
265
  topic="percent_change",
266
+ internal_answer=result,
267
+ ask="Find the new value after a percent increase or decrease.",
268
+ given=f"original = {original}, change = {pct}% {direction}",
269
+ method=[
270
  "Convert the percent to a decimal.",
271
+ "Use a multiplier.",
272
+ "For increase, multiply by 1 + p.",
273
+ "For decrease, multiply by 1 − p.",
274
+ ],
275
+ )
276
+
277
+ # --------------------------------------------------
278
+ # 6) ORIGINAL BEFORE CHANGE:
279
+ # "After a 20% increase, a number is 120"
280
+ # "After a 25% discount, the price is 90"
281
+ # --------------------------------------------------
282
+ m = re.search(
283
+ r"after\s+(?:an?\s+)?(\d+(?:\.\d+)?)\s*%\s*(increase|decrease|discount|markup|reduction|loss|gain).*?(?:is|becomes|equals|was|now\s+is)\s+(-?\d+(?:\.\d+)?)",
284
+ lower
285
+ )
286
+ if m:
287
+ pct = float(m.group(1))
288
+ kind = m.group(2)
289
+ final_value = float(m.group(3))
290
+ dec = _pct_to_decimal(pct)
291
+
292
+ if kind in ("increase", "markup", "gain"):
293
+ multiplier = 1 + dec
294
+ else:
295
+ multiplier = 1 - dec
296
+
297
+ if multiplier == 0:
298
+ return _fallback_percent()
299
+
300
+ result = final_value / multiplier
301
+ return _build_result(
302
+ topic="reverse_percent",
303
+ internal_answer=result,
304
+ ask="Find the original value before a percent change.",
305
+ given=f"final value = {final_value}, change = {pct}% {kind}",
306
+ method=[
307
+ "Translate the percent change into a multiplier.",
308
+ "Increase/markup/gain → original × (1 + p).",
309
+ "Decrease/discount/loss → original × (1 − p).",
310
+ "Solve by dividing the final value by that multiplier.",
311
  ],
312
  )
313
 
314
+ # --------------------------------------------------
315
+ # 7) PERCENT GREATER / LESS THAN
316
+ # "What percent greater is 80 than 50?"
317
+ # "80 is what percent greater than 50?"
318
+ # --------------------------------------------------
319
+ m = re.search(
320
+ r"what\s+percent\s+(greater|less)\s+(?:is\s+)?(-?\d+(?:\.\d+)?)\s+than\s+(-?\d+(?:\.\d+)?)",
321
+ lower
322
+ )
323
+ if m:
324
+ relation = m.group(1)
325
+ a = float(m.group(2))
326
+ b = float(m.group(3))
327
+ if b == 0:
328
+ return _fallback_percent()
329
+ result = ((a - b) / b) * 100.0
330
+ return _build_result(
331
+ topic="percent_comparison",
332
+ internal_answer=abs(result),
333
+ ask=f"Compare one value to another and express the difference as a percent of the reference value.",
334
+ given=f"compare {a} to reference {b}",
335
+ method=[
336
+ "Find the difference between the two values.",
337
+ "Use the second value as the reference base.",
338
+ "Compute difference ÷ reference × 100.",
339
+ f"Interpret the result as percent {relation}.",
340
+ ],
341
+ )
342
+
343
+ m = re.search(
344
+ r"(-?\d+(?:\.\d+)?)\s+is\s+what\s+percent\s+(greater|less)\s+than\s+(-?\d+(?:\.\d+)?)",
345
+ lower
346
+ )
347
+ if m:
348
+ a = float(m.group(1))
349
+ relation = m.group(2)
350
+ b = float(m.group(3))
351
+ if b == 0:
352
+ return _fallback_percent()
353
+ result = ((a - b) / b) * 100.0
354
+ return _build_result(
355
+ topic="percent_comparison",
356
+ internal_answer=abs(result),
357
+ ask="Find by what percent one value is greater or less than another.",
358
+ given=f"value = {a}, reference = {b}",
359
+ method=[
360
+ "Subtract to find the difference.",
361
+ "Divide by the reference value named after 'than'.",
362
+ "Multiply by 100 to get the percent comparison.",
363
+ f"Use the wording to label it as {relation}.",
364
+ ],
365
+ )
366
+
367
+ # --------------------------------------------------
368
+ # 8) DISCOUNT / MARKUP / TAX / TIP
369
+ # --------------------------------------------------
370
+ m = re.search(
371
+ r"(?:price|cost|bill|amount)\s+of\s+(-?\d+(?:\.\d+)?)\s+.*?(discount|markup|tax|tip)\s+of\s+(\d+(?:\.\d+)?)\s*%",
372
+ lower
373
+ )
374
+ if m:
375
+ base = float(m.group(1))
376
+ kind = m.group(2)
377
+ pct = float(m.group(3))
378
+ dec = _pct_to_decimal(pct)
379
+
380
+ if kind == "discount":
381
+ result = base * (1 - dec)
382
+ else:
383
+ result = base * (1 + dec)
384
+
385
+ return _build_result(
386
+ topic="percent_application",
387
+ internal_answer=result,
388
+ ask=f"Apply a {kind} percentage to a base amount.",
389
+ given=f"base = {base}, rate = {pct}% {kind}",
390
+ method=[
391
+ "Convert the percent to decimal form.",
392
+ "For discount, subtract the percent part from the base or multiply by 1 − p.",
393
+ "For markup, tax, or tip, add the percent part or multiply by 1 + p.",
394
+ ],
395
+ )
396
+
397
+ # --------------------------------------------------
398
+ # 9) PROFIT / LOSS
399
+ # --------------------------------------------------
400
+ m = re.search(
401
+ r"(?:cost price|cp|cost)\s+(-?\d+(?:\.\d+)?)\s+.*?(?:selling price|sp|sold for)\s+(-?\d+(?:\.\d+)?)",
402
+ lower
403
+ )
404
+ if m and ("profit" in lower or "loss" in lower or "percent" in lower or "%" in lower):
405
+ cost = float(m.group(1))
406
+ sell = float(m.group(2))
407
+ if cost == 0:
408
+ return _fallback_percent()
409
+ result = ((sell - cost) / cost) * 100.0
410
+ return _build_result(
411
+ topic="profit_loss",
412
+ internal_answer=result,
413
+ ask="Find the profit or loss percent relative to the cost price.",
414
+ given=f"cost price = {cost}, selling price = {sell}",
415
+ method=[
416
+ "Find profit/loss = selling price − cost price.",
417
+ "Use cost price as the denominator.",
418
+ "Compute (profit or loss) ÷ cost price × 100.",
419
+ ],
420
+ )
421
+
422
+ # --------------------------------------------------
423
+ # 10) SUCCESSIVE PERCENT CHANGES
424
+ # "increased by 20% and then decreased by 10%"
425
+ # --------------------------------------------------
426
+ m = re.search(
427
+ r"(-?\d+(?:\.\d+)?)\s+.*?(increase(?:d)?|decrease(?:d)?|reduced)\s+by\s+(\d+(?:\.\d+)?)\s*%.*?(increase(?:d)?|decrease(?:d)?|reduced)\s+by\s+(\d+(?:\.\d+)?)\s*%",
428
+ lower
429
+ )
430
+ if m:
431
+ original = float(m.group(1))
432
+ op1 = m.group(2)
433
+ p1 = _pct_to_decimal(float(m.group(3)))
434
+ op2 = m.group(4)
435
+ p2 = _pct_to_decimal(float(m.group(5)))
436
+
437
+ mult1 = 1 + p1 if "increase" in op1 else 1 - p1
438
+ mult2 = 1 + p2 if "increase" in op2 else 1 - p2
439
+ result = original * mult1 * mult2
440
+
441
+ return _build_result(
442
+ topic="successive_percent_change",
443
+ internal_answer=result,
444
+ ask="Apply two percent changes in sequence.",
445
+ given=f"start = {original}, first change = {m.group(3)}%, second change = {m.group(5)}%",
446
+ method=[
447
+ "Convert each percent to a decimal.",
448
+ "Turn each change into a multiplier.",
449
+ "Apply the first multiplier, then apply the second to the new value.",
450
+ "Do not add or subtract the percentages directly unless the problem specifically asks for net rate in a special setup.",
451
+ ],
452
+ )
453
+
454
+ # --------------------------------------------------
455
+ # 11) PERCENT DIFFERENCE
456
+ # --------------------------------------------------
457
+ if "percent difference" in lower:
458
+ m = re.search(
459
+ r"between\s+(-?\d+(?:\.\d+)?)\s+and\s+(-?\d+(?:\.\d+)?)",
460
+ lower
461
+ )
462
+ if m:
463
+ a = float(m.group(1))
464
+ b = float(m.group(2))
465
+ avg = (a + b) / 2.0
466
+ if avg == 0:
467
+ return _fallback_percent()
468
+ result = abs(a - b) / avg * 100.0
469
+ return _build_result(
470
+ topic="percent_difference",
471
+ internal_answer=result,
472
+ ask="Find the percent difference between two values.",
473
+ given=f"values = {a} and {b}",
474
+ method=[
475
+ "Find the absolute difference between the two values.",
476
+ "Find the average of the two values.",
477
+ "Use percent difference = difference ÷ average × 100.",
478
+ "Keep this separate from percent change, which uses the original value as the denominator.",
479
+ ],
480
+ )
481
+
482
+ # --------------------------------------------------
483
+ # 12) SIMPLE INTEREST
484
+ # --------------------------------------------------
485
+ if "simple interest" in lower:
486
+ m = re.search(
487
+ r"(-?\d+(?:\.\d+)?)\s+.*?(\d+(?:\.\d+)?)\s*%\s+.*?(\d+(?:\.\d+)?)\s*(year|years|month|months)",
488
+ lower
489
+ )
490
+ if m:
491
+ principal = float(m.group(1))
492
+ rate = float(m.group(2))
493
+ time = float(m.group(3))
494
+ unit = m.group(4)
495
+
496
+ t_years = time / 12.0 if "month" in unit else time
497
+ result = principal * _pct_to_decimal(rate) * t_years
498
+
499
+ return _build_result(
500
+ topic="simple_interest",
501
+ internal_answer=result,
502
+ ask="Find interest earned using the simple interest formula.",
503
+ given=f"principal = {principal}, rate = {rate}%, time = {time} {unit}",
504
+ method=[
505
+ "Use simple interest = principal × rate × time.",
506
+ "Convert the rate to decimal form.",
507
+ "Make sure time is in the same units as the rate.",
508
+ "If the rate is annual and time is in months, convert months to years first.",
509
  ],
510
  )
511
 
512
+ # --------------------------------------------------
513
+ # 13) COMPOUND INTEREST
514
+ # --------------------------------------------------
515
+ if "compound interest" in lower or "compounded" in lower:
516
+ m = re.search(
517
+ r"(-?\d+(?:\.\d+)?)\s+.*?(\d+(?:\.\d+)?)\s*%\s+.*?compounded\s+(annually|semiannually|quarterly|monthly).*?(\d+(?:\.\d+)?)\s+year",
518
+ lower
519
+ )
520
+ if m:
521
+ principal = float(m.group(1))
522
+ rate = float(m.group(2))
523
+ comp = m.group(3)
524
+ years = float(m.group(4))
525
+
526
+ c_map = {
527
+ "annually": 1,
528
+ "semiannually": 2,
529
+ "quarterly": 4,
530
+ "monthly": 12,
531
+ }
532
+ c = c_map[comp]
533
+ result = principal * ((1 + _pct_to_decimal(rate) / c) ** (c * years))
534
+
535
+ return _build_result(
536
+ topic="compound_interest",
537
+ internal_answer=result,
538
+ ask="Find the final balance after compound interest.",
539
+ given=f"principal = {principal}, rate = {rate}%, compounded {comp}, time = {years} years",
540
+ method=[
541
+ "Use the compound interest form A = P(1 + r/c)^(ct).",
542
+ "Convert the rate to decimal form.",
543
+ "Match the compounding frequency to the correct value of c.",
544
+ "Substitute carefully before evaluating.",
545
+ ],
546
+ )
547
+
548
+ # --------------------------------------------------
549
+ # 14) PERCENTILE
550
+ # --------------------------------------------------
551
+ if "percentile" in lower:
552
+ # Simple interpretation pattern:
553
+ # "80th percentile out of 120"
554
+ m = re.search(
555
+ r"(\d+(?:\.\d+)?)(?:st|nd|rd|th)\s+percentile\s+out\s+of\s+(\d+(?:\.\d+)?)",
556
+ lower
557
+ )
558
+ if m:
559
+ pct = float(m.group(1))
560
+ total = float(m.group(2))
561
+ result = _pct_to_decimal(pct) * total
562
+ return _build_result(
563
+ topic="percentile",
564
+ internal_answer=result,
565
+ ask="Interpret a percentile as the proportion of the group below that score.",
566
+ given=f"{pct}th percentile out of {total}",
567
+ method=[
568
+ "Convert the percentile to a decimal proportion.",
569
+ "Multiply by the total group size to estimate how many are below that position.",
570
+ "Check the wording carefully: percentile usually refers to the percentage of values below a score.",
571
+ ],
572
+ )
573
+
574
+ # --------------------------------------------------
575
+ # 15) GENERIC HIDDEN PERCENT LANGUAGE
576
+ # "out of", "of", "remaining", "sale", etc.
577
+ # --------------------------------------------------
578
+ if len(nums) >= 2 and any(k in lower for k in ["out of", "remaining", "sale", "discount", "tax", "tip", "profit", "loss"]):
579
+ return SolverResult(
580
+ domain="quant",
581
+ solved=False,
582
+ topic="percent",
583
+ answer_value=None,
584
+ internal_answer=None,
585
+ steps=[
586
+ "This looks like a percent-style word problem.",
587
+ "What the question is asking: first decide whether the unknown is the part, whole, rate, original value, or changed value.",
588
+ "Useful checks:",
589
+ "• 'out of' often signals part/whole",
590
+ "• 'discount', 'tax', 'tip', 'markup' often signal multiplier use",
591
+ "• 'profit'/'loss' uses cost price as the base",
592
+ "• 'from ... to ...' often signals percent change",
593
+ ],
594
+ )
595
+
596
+ return _fallback_percent()