j-js commited on
Commit
d1c397a
·
verified ·
1 Parent(s): 0e39b7f

Update solver_remainder.py

Browse files
Files changed (1) hide show
  1. solver_remainder.py +618 -32
solver_remainder.py CHANGED
@@ -1,66 +1,652 @@
1
  from __future__ import annotations
2
 
 
 
3
  import re
4
- from typing import Optional
5
 
6
  from models import SolverResult
7
 
8
 
9
- def solve_remainder(text: str) -> Optional[SolverResult]:
10
- lower = (text or "").lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- if "remainder" not in lower and "mod" not in lower:
 
 
 
 
 
 
 
 
 
 
13
  return None
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # "remainder when 17 is divided by 5"
16
  m = re.search(
17
- r"remainder.*?when\s+(-?\d+)\s+(?:is\s+)?divided\s+by\s+(-?\d+)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  lower,
19
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  if not m:
21
  m = re.search(
22
- r"(-?\d+)\s*(?:mod|%)\s*(-?\d+)",
 
 
 
 
23
  lower,
24
  )
25
 
26
  if m:
27
- a = int(m.group(1))
28
- b = int(m.group(2))
29
- if b == 0:
30
- return None
31
- result = a % b
32
- return SolverResult(
33
- domain="quant",
34
- solved=True,
35
- topic="remainder",
36
- answer_value=str(result),
 
 
 
 
 
 
 
 
37
  internal_answer=str(result),
38
  steps=[
39
- "Use the division algorithm.",
40
- "The remainder is what is left after dividing by the divisor.",
 
41
  ],
42
  )
43
 
44
- # "When n is divided by 5 the remainder is 2. What is remainder when 3n is divided by 5?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  m = re.search(
46
- r"remainder\s+is\s+(\d+).*?what\s+is\s+the\s+remainder\s+when\s+(\d+)n\s+is\s+divided\s+by\s+(\d+)",
47
  lower,
48
  )
49
  if m:
50
- r = int(m.group(1))
51
- mult = int(m.group(2))
52
- mod = int(m.group(3))
53
- result = (mult * r) % mod
54
- return SolverResult(
55
- domain="quant",
56
- solved=True,
57
- topic="remainder",
58
- answer_value=str(result),
59
- internal_answer=str(result),
60
  steps=[
61
- "Replace n by its remainder class modulo the divisor.",
62
- "Multiply the remainder and reduce again modulo the divisor.",
 
63
  ],
64
  )
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  return None
 
1
  from __future__ import annotations
2
 
3
+ import ast
4
+ import math
5
  import re
6
+ from typing import Optional, List, Tuple
7
 
8
  from models import SolverResult
9
 
10
 
11
+ # ----------------------------
12
+ # Helpers
13
+ # ----------------------------
14
+
15
+ def _clean(text: str) -> str:
16
+ t = text or ""
17
+ t = t.replace("−", "-").replace("–", "-").replace("—", "-")
18
+ t = t.replace("×", "*").replace("·", "*")
19
+ t = t.replace("÷", "/")
20
+ t = t.replace("^", "**")
21
+ t = t.replace("≡", " congruent ")
22
+ t = t.replace("modulo", "mod")
23
+ t = t.replace("modulus", "mod")
24
+ t = re.sub(r"\s+", " ", t)
25
+ return t.strip()
26
+
27
+
28
+ def _gcd(a: int, b: int) -> int:
29
+ return math.gcd(a, b)
30
+
31
+
32
+ def _lcm(a: int, b: int) -> int:
33
+ return abs(a * b) // math.gcd(a, b) if a and b else 0
34
 
35
+
36
+ def _normalize_remainder(r: int, m: int) -> int:
37
+ return r % m
38
+
39
+
40
+ def _safe_int_eval(expr: str) -> Optional[int]:
41
+ """
42
+ Safely evaluate simple integer arithmetic expressions.
43
+ Supports: +, -, *, //, / (only if exact), %, **, parentheses, unary +/-.
44
+ """
45
+ if not expr:
46
  return None
47
 
48
+ expr = expr.strip()
49
+ expr = expr.replace("^", "**")
50
+
51
+ if re.search(r"[^0-9\+\-\*\/%\(\)\s]", expr):
52
+ return None
53
+
54
+ try:
55
+ node = ast.parse(expr, mode="eval")
56
+ except Exception:
57
+ return None
58
+
59
+ def _eval(n):
60
+ if isinstance(n, ast.Expression):
61
+ return _eval(n.body)
62
+
63
+ if isinstance(n, ast.Constant):
64
+ if isinstance(n.value, int):
65
+ return n.value
66
+ return None
67
+
68
+ if isinstance(n, ast.UnaryOp) and isinstance(n.op, (ast.UAdd, ast.USub)):
69
+ v = _eval(n.operand)
70
+ if v is None:
71
+ return None
72
+ return +v if isinstance(n.op, ast.UAdd) else -v
73
+
74
+ if isinstance(n, ast.BinOp):
75
+ left = _eval(n.left)
76
+ right = _eval(n.right)
77
+ if left is None or right is None:
78
+ return None
79
+
80
+ if isinstance(n.op, ast.Add):
81
+ return left + right
82
+ if isinstance(n.op, ast.Sub):
83
+ return left - right
84
+ if isinstance(n.op, ast.Mult):
85
+ return left * right
86
+ if isinstance(n.op, ast.Mod):
87
+ if right == 0:
88
+ return None
89
+ return left % right
90
+ if isinstance(n.op, ast.Pow):
91
+ if right < 0 or right > 10000:
92
+ return None
93
+ return left ** right
94
+ if isinstance(n.op, ast.FloorDiv):
95
+ if right == 0:
96
+ return None
97
+ return left // right
98
+ if isinstance(n.op, ast.Div):
99
+ if right == 0:
100
+ return None
101
+ if left % right != 0:
102
+ return None
103
+ return left // right
104
+
105
+ return None
106
+
107
+ try:
108
+ val = _eval(node)
109
+ if isinstance(val, int):
110
+ return val
111
+ except Exception:
112
+ return None
113
+ return None
114
+
115
+
116
+ def _pow_mod(base: int, exp: int, mod: int) -> Optional[int]:
117
+ if mod == 0 or exp < 0:
118
+ return None
119
+ return pow(base, exp, mod)
120
+
121
+
122
+ def _extract_choices(text: str) -> List[int]:
123
+ """
124
+ Pull numeric answer choices if they exist.
125
+ """
126
+ nums = re.findall(r"(?:^|[\s\(])(-?\d+)(?:[\s\),\.]|$)", text)
127
+ out = []
128
+ for n in nums:
129
+ try:
130
+ out.append(int(n))
131
+ except Exception:
132
+ pass
133
+ return out
134
+
135
+
136
+ def _first_crt_solution(r1: int, m1: int, r2: int, m2: int) -> Optional[int]:
137
+ """
138
+ Find the smallest nonnegative x satisfying:
139
+ x ≡ r1 (mod m1)
140
+ x ≡ r2 (mod m2)
141
+ Return None if inconsistent.
142
+ """
143
+ if m1 <= 0 or m2 <= 0:
144
+ return None
145
+
146
+ r1 %= m1
147
+ r2 %= m2
148
+
149
+ g = math.gcd(m1, m2)
150
+ if (r1 - r2) % g != 0:
151
+ return None
152
+
153
+ step = m1
154
+ limit_mod = _lcm(m1, m2)
155
+ x = r1
156
+ for _ in range(limit_mod // step + 2):
157
+ if x % m2 == r2:
158
+ return x
159
+ x += step
160
+ return None
161
+
162
+
163
+ def _make_result(
164
+ internal_answer: str,
165
+ steps: List[str],
166
+ topic: str = "remainder",
167
+ solved: bool = True,
168
+ answer_value: Optional[str] = None,
169
+ answer_letter: Optional[str] = None,
170
+ ) -> SolverResult:
171
+ # keep the true answer internal; downstream formatter should avoid printing it directly
172
+ return SolverResult(
173
+ domain="quant",
174
+ solved=solved,
175
+ topic=topic,
176
+ answer_value=answer_value if answer_value is not None else internal_answer,
177
+ answer_letter=answer_letter,
178
+ internal_answer=internal_answer,
179
+ steps=steps,
180
+ )
181
+
182
+
183
+ def _mentions_remainders(lower: str) -> bool:
184
+ triggers = [
185
+ "remainder",
186
+ "mod ",
187
+ " mod",
188
+ "divisible",
189
+ "divided by",
190
+ "leaves",
191
+ "left over",
192
+ "last digit",
193
+ "units digit",
194
+ "tens digit",
195
+ "ones digit",
196
+ ]
197
+ return any(t in lower for t in triggers)
198
+
199
+
200
+ # ----------------------------
201
+ # Core patterns
202
+ # ----------------------------
203
+
204
+ def _solve_direct_numeric_remainder(text: str) -> Optional[SolverResult]:
205
+ lower = text.lower()
206
+
207
  # "remainder when 17 is divided by 5"
208
  m = re.search(
209
+ r"remainder.*?when\s+(.+?)\s+(?:is\s+)?divided\s+by\s+(-?\d+)",
210
+ lower,
211
+ )
212
+ if m:
213
+ expr = m.group(1).strip()
214
+ mod = int(m.group(2))
215
+ val = _safe_int_eval(expr)
216
+ if val is not None and mod != 0:
217
+ result = val % mod
218
+ return _make_result(
219
+ internal_answer=str(result),
220
+ steps=[
221
+ "Write the dividend in quotient–remainder form: dividend = divisor × quotient + remainder.",
222
+ "Reduce the computed value modulo the divisor.",
223
+ "Keep the remainder nonnegative and smaller than the divisor.",
224
+ ],
225
+ )
226
+
227
+ # "123 mod 10" or "123 % 10"
228
+ m = re.search(r"(-?\d+(?:\s*[\+\-\*\/%]\s*-?\d+|\s*\*\*\s*-?\d+)*)\s*(?:mod|%)\s*(-?\d+)", lower)
229
+ if m:
230
+ expr = m.group(1).strip()
231
+ mod = int(m.group(2))
232
+ val = _safe_int_eval(expr)
233
+ if val is not None and mod != 0:
234
+ result = val % mod
235
+ return _make_result(
236
+ internal_answer=str(result),
237
+ steps=[
238
+ "Interpret the expression as a modulo calculation.",
239
+ "Evaluate the expression carefully, then reduce modulo the divisor.",
240
+ "Adjust to the standard remainder range from 0 up to divisor - 1.",
241
+ ],
242
+ )
243
+
244
+ return None
245
+
246
+
247
+ def _solve_last_digit_patterns(text: str) -> Optional[SolverResult]:
248
+ lower = text.lower()
249
+
250
+ # "last digit of 12345" / "ones digit of 12345"
251
+ m = re.search(r"(?:last|ones|units)\s+digit\s+of\s+(.+)", lower)
252
+ if m:
253
+ expr = m.group(1).strip(" ?.")
254
+ val = _safe_int_eval(expr)
255
+ if val is not None:
256
+ result = val % 10
257
+ return _make_result(
258
+ internal_answer=str(result),
259
+ steps=[
260
+ "The last digit is the remainder upon division by 10.",
261
+ "So reduce the number modulo 10 rather than working with the entire value.",
262
+ "That gives the required digit.",
263
+ ],
264
+ topic="remainder",
265
+ )
266
+
267
+ # "tens digit of x" given remainder 30 when divided by 100 is harder DS style;
268
+ # here support simple explicit numeric prompts only.
269
+ m = re.search(r"remainder.*?divided by 100.*?is\s+(\d+)", lower)
270
+ if m and ("tens digit" in lower):
271
+ rem = int(m.group(1))
272
+ tens = (rem // 10) % 10
273
+ return _make_result(
274
+ internal_answer=str(tens),
275
+ steps=[
276
+ "A remainder upon division by 100 preserves the last two digits.",
277
+ "The tens digit is therefore the tens digit of that two-digit remainder.",
278
+ "Read the target digit directly from the remainder.",
279
+ ],
280
+ topic="remainder",
281
+ )
282
+
283
+ return None
284
+
285
+
286
+ def _solve_known_remainder_linear_transform(text: str) -> Optional[SolverResult]:
287
+ lower = text.lower()
288
+
289
+ # n divided by m leaves remainder r; what remainder when k*n is divided by m?
290
+ m = re.search(
291
+ r"remainder\s+(?:is|of)\s+(\d+)\s+when\s+\w+\s+is\s+divided\s+by\s+(\d+).*?"
292
+ r"what\s+is\s+the\s+remainder\s+when\s+(\d+)\s*\*?\s*\w+\s+is\s+divided\s+by\s+(\d+)",
293
+ lower,
294
+ )
295
+ if m:
296
+ r = int(m.group(1))
297
+ old_mod = int(m.group(2))
298
+ mult = int(m.group(3))
299
+ new_mod = int(m.group(4))
300
+ # only safe when divisor is same or the old divisor term remains divisible by new divisor
301
+ if old_mod % new_mod == 0:
302
+ result = (mult * r) % new_mod
303
+ return _make_result(
304
+ internal_answer=str(result),
305
+ steps=[
306
+ "Replace the variable by divisor × quotient + known remainder.",
307
+ "Multiply through, then ignore the term guaranteed to stay divisible by the new divisor.",
308
+ "Reduce the remaining expression modulo the requested divisor.",
309
+ ],
310
+ )
311
+
312
+ # n leaves remainder r on division by m. what is remainder when (a*n + b) is divided by m?
313
+ m = re.search(
314
+ r"(?:(?:when|if)\s+)?(?:positive\s+integer\s+)?([a-z])\s+(?:is\s+)?divided\s+by\s+(\d+)\s+"
315
+ r"(?:has|leaves|gives)\s+(?:a\s+)?remainder\s+(?:of\s+)?(\d+).*?"
316
+ r"what\s+is\s+the\s+remainder\s+when\s+([\-]?\d*)\s*\*?\s*\1\s*([+\-]\s*\d+)?\s+is\s+divided\s+by\s+(\d+)",
317
+ lower,
318
+ )
319
+ if m:
320
+ mod1 = int(m.group(2))
321
+ r = int(m.group(3))
322
+ a_txt = m.group(4).strip()
323
+ b_txt = (m.group(5) or "").replace(" ", "")
324
+ mod2 = int(m.group(6))
325
+
326
+ if a_txt in ("", "+"):
327
+ a = 1
328
+ elif a_txt == "-":
329
+ a = -1
330
+ else:
331
+ a = int(a_txt)
332
+
333
+ b = int(b_txt) if b_txt else 0
334
+
335
+ if mod1 == mod2:
336
+ result = (a * r + b) % mod2
337
+ return _make_result(
338
+ internal_answer=str(result),
339
+ steps=[
340
+ "Substitute the variable with its remainder class modulo the divisor.",
341
+ "Apply the linear transformation to the remainder instead of to the full number.",
342
+ "Reduce once more to keep the answer in the standard remainder range.",
343
+ ],
344
+ )
345
+
346
+ # book-style: remainder 7 when n divided by 18; find remainder when n divided by 6
347
+ m = re.search(
348
+ r"remainder\s+(?:is|of)\s+(\d+)\s+when\s+([a-z])\s+is\s+divided\s+by\s+(\d+).*?"
349
+ r"what\s+is\s+the\s+remainder\s+when\s+\2\s+is\s+divided\s+by\s+(\d+)",
350
  lower,
351
  )
352
+ if m:
353
+ r = int(m.group(1))
354
+ big_mod = int(m.group(3))
355
+ small_mod = int(m.group(4))
356
+ if big_mod % small_mod == 0:
357
+ result = r % small_mod
358
+ return _make_result(
359
+ internal_answer=str(result),
360
+ steps=[
361
+ "Write the number as big divisor × quotient + known remainder.",
362
+ "If the big divisor is a multiple of the smaller divisor, that first term contributes no new remainder.",
363
+ "So reduce the known remainder by the smaller divisor.",
364
+ ],
365
+ )
366
+
367
+ return None
368
+
369
+
370
+ def _solve_power_remainder(text: str) -> Optional[SolverResult]:
371
+ lower = text.lower()
372
+
373
+ # "when 51^25 is divided by 13"
374
+ m = re.search(
375
+ r"when\s+(-?\d+)\s*\*\*\s*(\d+)\s+is\s+divided\s+by\s+(\d+)", lower
376
+ )
377
  if not m:
378
  m = re.search(
379
+ r"when\s+(-?\d+)\s*\^\s*(\d+)\s+is\s+divided\s+by\s+(\d+)", text.lower()
380
+ )
381
+ if not m:
382
+ m = re.search(
383
+ r"remainder.*?when\s+(-?\d+)\s*(?:\^|\*\*)\s*(\d+)\s+(?:is\s+)?divided\s+by\s+(\d+)",
384
  lower,
385
  )
386
 
387
  if m:
388
+ base = int(m.group(1))
389
+ exp = int(m.group(2))
390
+ mod = int(m.group(3))
391
+ if mod != 0:
392
+ result = pow(base, exp, mod)
393
+ return _make_result(
394
+ internal_answer=str(result),
395
+ steps=[
396
+ "Reduce the base modulo the divisor first.",
397
+ "Then use modular exponentiation or a repeating cycle pattern.",
398
+ "Only the remainder class matters, not the full power.",
399
+ ],
400
+ )
401
+
402
+ # special pattern: prime > 3, remainder when n^2 is divided by 12
403
+ if "prime number greater than 3" in lower and ("n^2" in text.lower() or "n**2" in lower) and "divided by 12" in lower:
404
+ result = 1
405
+ return _make_result(
406
  internal_answer=str(result),
407
  steps=[
408
+ "A prime greater than 3 is not divisible by 2 or 3.",
409
+ "So it must be congruent to 1, 5, 7, or 11 modulo 12.",
410
+ "Squaring any of those residue classes gives the same remainder modulo 12.",
411
  ],
412
  )
413
 
414
+ return None
415
+
416
+
417
+ def _solve_two_congruences_same_variable(text: str) -> Optional[SolverResult]:
418
+ lower = text.lower()
419
+
420
+ # Example:
421
+ # n leaves remainder 4 when divided by 6 and remainder 3 when divided by 5
422
+ matches = re.findall(
423
+ r"([a-z])\s+(?:is\s+)?(?:greater\s+than\s+\d+\s+and\s+)?(?:leaves|has|gives)\s+(?:a\s+)?remainder\s+(\d+)\s+"
424
+ r"(?:after\s+division\s+by|when\s+divided\s+by)\s+(\d+)",
425
+ lower,
426
+ )
427
+
428
+ if len(matches) >= 2:
429
+ v1, r1, m1 = matches[0]
430
+ v2, r2, m2 = matches[1]
431
+ if v1 == v2:
432
+ r1, m1, r2, m2 = int(r1), int(m1), int(r2), int(m2)
433
+ first = _first_crt_solution(r1, m1, r2, m2)
434
+ if first is not None:
435
+ l = _lcm(m1, m2)
436
+
437
+ # ask for remainder when same variable divided by lcm or another modulus
438
+ q = re.search(
439
+ rf"what\s+is\s+the\s+remainder.*?{v1}.*?divided\s+by\s+(\d+)",
440
+ lower,
441
+ )
442
+ if q:
443
+ target_mod = int(q.group(1))
444
+ result = first % target_mod
445
+ return _make_result(
446
+ internal_answer=str(result),
447
+ steps=[
448
+ "Translate each condition into a congruence.",
449
+ "Find the first common value that satisfies both patterns, then express all solutions using the least common multiple of the divisors.",
450
+ "Reduce that shared residue class by the requested divisor.",
451
+ ],
452
+ )
453
+
454
+ # if question explicitly asks remainder on division by lcm
455
+ result = first % l
456
+ return _make_result(
457
+ internal_answer=str(result),
458
+ steps=[
459
+ "Find the overlap of the two arithmetic patterns.",
460
+ "That overlap becomes the residue class modulo the least common multiple of the divisors.",
461
+ "The remainder on division by that common modulus is the first shared residue.",
462
+ ],
463
+ )
464
+
465
+ return None
466
+
467
+
468
+ def _solve_difference_same_remainders(text: str) -> Optional[SolverResult]:
469
+ lower = text.lower()
470
+
471
+ # x and y have same remainder mod 5 and mod 7; factor of x-y?
472
+ m1 = re.search(
473
+ r"when\s+positive\s+integer\s+x\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+).*?"
474
+ r"when\s+x\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+)",
475
+ lower,
476
+ re.DOTALL,
477
+ )
478
+ m2 = re.search(
479
+ r"when\s+positive\s+integer\s+y\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+).*?"
480
+ r"when\s+y\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+)",
481
+ lower,
482
+ re.DOTALL,
483
+ )
484
+
485
+ if m1 and m2:
486
+ ax1, ar1, ax2, ar2 = map(int, m1.groups())
487
+ ay1, br1, ay2, br2 = map(int, m2.groups())
488
+
489
+ if ax1 == ay1 and ax2 == ay2 and ar1 == br1 and ar2 == br2:
490
+ must_factor = _lcm(ax1, ax2)
491
+ return _make_result(
492
+ internal_answer=str(must_factor),
493
+ steps=[
494
+ "If two numbers leave the same remainder upon division by a modulus, their difference is divisible by that modulus.",
495
+ "Apply that idea to each divisor separately.",
496
+ "So the difference must be divisible by the least common multiple of those divisors.",
497
+ ],
498
+ )
499
+
500
+ return None
501
+
502
+
503
+ def _solve_decimal_quotient_remainder(text: str) -> Optional[SolverResult]:
504
+ lower = text.lower()
505
+
506
+ # s/t = 64.12 ; what could be the remainder when s is divided by t?
507
  m = re.search(
508
+ r"([a-z])\s*/\s*([a-z])\s*=\s*(\d+)\.(\d+).*?remainder\s+when\s+\1\s+is\s+divided\s+by\s+\2",
509
  lower,
510
  )
511
  if m:
512
+ # If s/t = q + frac, then remainder / t = frac.
513
+ # For 64.12 = 64 + 12/100 = 64 + 3/25, so remainder must be a multiple of 3.
514
+ frac_num = int(m.group(4))
515
+ frac_den = 10 ** len(m.group(4))
516
+ g = _gcd(frac_num, frac_den)
517
+ num = frac_num // g
518
+ # remainder = num * k, t = den * k
519
+ return _make_result(
520
+ internal_answer=str(num),
 
521
  steps=[
522
+ "Use dividend = divisor × quotient + remainder, then divide both sides by the divisor.",
523
+ "The decimal part of the quotient equals remainder/divisor.",
524
+ "Reduce the fractional part to lowest terms; the numerator controls the remainder pattern.",
525
  ],
526
  )
527
 
528
+ return None
529
+
530
+
531
+ def _solve_divisibility_from_remainder_expression(text: str) -> Optional[SolverResult]:
532
+ lower = text.lower()
533
+
534
+ # Is n divisible by d? / what is remainder when expression divided by d?
535
+ # x^3 - x pattern
536
+ if ("x^3 - x" in text.lower() or "x**3 - x" in lower or "x^3-x" in text.lower()) and "divisible by 8" in lower:
537
+ return _make_result(
538
+ internal_answer="yes",
539
+ steps=[
540
+ "Factor the expression into consecutive integers.",
541
+ "Among consecutive integers, use parity structure to identify enough factors of 2.",
542
+ "That guarantees divisibility by the target power of 2.",
543
+ ],
544
+ topic="remainder",
545
+ answer_value="yes",
546
+ )
547
+
548
+ return None
549
+
550
+
551
+ def _solve_smaller_dividend_than_divisor(text: str) -> Optional[SolverResult]:
552
+ lower = text.lower()
553
+
554
+ m = re.search(
555
+ r"remainder.*?when\s+(\d+)\s+(?:is\s+)?divided\s+by\s+(\d+)",
556
+ lower,
557
+ )
558
+ if m:
559
+ a = int(m.group(1))
560
+ b = int(m.group(2))
561
+ if 0 <= a < b:
562
+ return _make_result(
563
+ internal_answer=str(a),
564
+ steps=[
565
+ "If the dividend is smaller than the divisor, the quotient is 0.",
566
+ "So the entire dividend becomes the remainder.",
567
+ "No further reduction is needed.",
568
+ ],
569
+ )
570
+
571
+ return None
572
+
573
+
574
+ # ----------------------------
575
+ # Fallback pattern helpers
576
+ # ----------------------------
577
+
578
+ def _solve_generic_known_remainder_question(text: str) -> Optional[SolverResult]:
579
+ lower = text.lower()
580
+
581
+ # Parse statements like:
582
+ # "n divided by 5 has remainder 2"
583
+ known = re.findall(
584
+ r"([a-z])\s+(?:is\s+)?divided\s+by\s+(\d+)\s+(?:has|gives|leaves)\s+(?:a\s+)?remainder\s+(?:of\s+)?(\d+)",
585
+ lower,
586
+ )
587
+ if not known:
588
+ known = re.findall(
589
+ r"remainder\s+(?:is|of)\s+(\d+)\s+when\s+([a-z])\s+is\s+divided\s+by\s+(\d+)",
590
+ lower,
591
+ )
592
+ known = [(v, m, r) for (r, v, m) in known]
593
+
594
+ if not known:
595
+ return None
596
+
597
+ var, mod, rem = known[0]
598
+ mod = int(mod)
599
+ rem = int(rem)
600
+
601
+ # ask for variable itself divided by another modulus
602
+ q = re.search(rf"what\s+is\s+the\s+remainder\s+when\s+{var}\s+is\s+divided\s+by\s+(\d+)", lower)
603
+ if q:
604
+ target = int(q.group(1))
605
+ if mod % target == 0:
606
+ result = rem % target
607
+ return _make_result(
608
+ internal_answer=str(result),
609
+ steps=[
610
+ "Rewrite the number as divisor × quotient + remainder.",
611
+ "Check whether the known divisor is a multiple of the target divisor.",
612
+ "Then reduce the known remainder by the target divisor.",
613
+ ],
614
+ )
615
+
616
+ return None
617
+
618
+
619
+ # ----------------------------
620
+ # Main entry
621
+ # ----------------------------
622
+
623
+ def solve_remainder(text: str) -> Optional[SolverResult]:
624
+ raw = text or ""
625
+ cleaned = _clean(raw)
626
+ lower = cleaned.lower()
627
+
628
+ if not _mentions_remainders(lower):
629
+ return None
630
+
631
+ solvers = [
632
+ _solve_direct_numeric_remainder,
633
+ _solve_smaller_dividend_than_divisor,
634
+ _solve_last_digit_patterns,
635
+ _solve_known_remainder_linear_transform,
636
+ _solve_power_remainder,
637
+ _solve_two_congruences_same_variable,
638
+ _solve_difference_same_remainders,
639
+ _solve_decimal_quotient_remainder,
640
+ _solve_divisibility_from_remainder_expression,
641
+ _solve_generic_known_remainder_question,
642
+ ]
643
+
644
+ for fn in solvers:
645
+ try:
646
+ out = fn(cleaned)
647
+ if out is not None:
648
+ return out
649
+ except Exception:
650
+ continue
651
+
652
  return None