| from __future__ import annotations |
|
|
| import ast |
| import math |
| import re |
| from typing import Optional, List, Tuple |
|
|
| from models import SolverResult |
|
|
|
|
| |
| |
| |
|
|
| def _clean(text: str) -> str: |
| t = text or "" |
| t = t.replace("−", "-").replace("–", "-").replace("—", "-") |
| t = t.replace("×", "*").replace("·", "*") |
| t = t.replace("÷", "/") |
| t = t.replace("^", "**") |
| t = t.replace("≡", " congruent ") |
| t = t.replace("modulo", "mod") |
| t = t.replace("modulus", "mod") |
| t = re.sub(r"\s+", " ", t) |
| return t.strip() |
|
|
|
|
| def _gcd(a: int, b: int) -> int: |
| return math.gcd(a, b) |
|
|
|
|
| def _lcm(a: int, b: int) -> int: |
| return abs(a * b) // math.gcd(a, b) if a and b else 0 |
|
|
|
|
| def _normalize_remainder(r: int, m: int) -> int: |
| return r % m |
|
|
|
|
| def _safe_int_eval(expr: str) -> Optional[int]: |
| """ |
| Safely evaluate simple integer arithmetic expressions. |
| Supports: +, -, *, //, / (only if exact), %, **, parentheses, unary +/-. |
| """ |
| if not expr: |
| return None |
|
|
| expr = expr.strip() |
| expr = expr.replace("^", "**") |
|
|
| if re.search(r"[^0-9\+\-\*\/%\(\)\s]", expr): |
| return None |
|
|
| try: |
| node = ast.parse(expr, mode="eval") |
| except Exception: |
| return None |
|
|
| def _eval(n): |
| if isinstance(n, ast.Expression): |
| return _eval(n.body) |
|
|
| if isinstance(n, ast.Constant): |
| if isinstance(n.value, int): |
| return n.value |
| return None |
|
|
| if isinstance(n, ast.UnaryOp) and isinstance(n.op, (ast.UAdd, ast.USub)): |
| v = _eval(n.operand) |
| if v is None: |
| return None |
| return +v if isinstance(n.op, ast.UAdd) else -v |
|
|
| if isinstance(n, ast.BinOp): |
| left = _eval(n.left) |
| right = _eval(n.right) |
| if left is None or right is None: |
| return None |
|
|
| if isinstance(n.op, ast.Add): |
| return left + right |
| if isinstance(n.op, ast.Sub): |
| return left - right |
| if isinstance(n.op, ast.Mult): |
| return left * right |
| if isinstance(n.op, ast.Mod): |
| if right == 0: |
| return None |
| return left % right |
| if isinstance(n.op, ast.Pow): |
| if right < 0 or right > 10000: |
| return None |
| return left ** right |
| if isinstance(n.op, ast.FloorDiv): |
| if right == 0: |
| return None |
| return left // right |
| if isinstance(n.op, ast.Div): |
| if right == 0: |
| return None |
| if left % right != 0: |
| return None |
| return left // right |
|
|
| return None |
|
|
| try: |
| val = _eval(node) |
| if isinstance(val, int): |
| return val |
| except Exception: |
| return None |
| return None |
|
|
|
|
| def _pow_mod(base: int, exp: int, mod: int) -> Optional[int]: |
| if mod == 0 or exp < 0: |
| return None |
| return pow(base, exp, mod) |
|
|
|
|
| def _extract_choices(text: str) -> List[int]: |
| """ |
| Pull numeric answer choices if they exist. |
| """ |
| nums = re.findall(r"(?:^|[\s\(])(-?\d+)(?:[\s\),\.]|$)", text) |
| out = [] |
| for n in nums: |
| try: |
| out.append(int(n)) |
| except Exception: |
| pass |
| return out |
|
|
|
|
| def _first_crt_solution(r1: int, m1: int, r2: int, m2: int) -> Optional[int]: |
| """ |
| Find the smallest nonnegative x satisfying: |
| x ≡ r1 (mod m1) |
| x ≡ r2 (mod m2) |
| Return None if inconsistent. |
| """ |
| if m1 <= 0 or m2 <= 0: |
| return None |
|
|
| r1 %= m1 |
| r2 %= m2 |
|
|
| g = math.gcd(m1, m2) |
| if (r1 - r2) % g != 0: |
| return None |
|
|
| step = m1 |
| limit_mod = _lcm(m1, m2) |
| x = r1 |
| for _ in range(limit_mod // step + 2): |
| if x % m2 == r2: |
| return x |
| x += step |
| return None |
|
|
|
|
| def _make_result( |
| internal_answer: str, |
| steps: List[str], |
| topic: str = "remainder", |
| solved: bool = True, |
| answer_value: Optional[str] = None, |
| answer_letter: Optional[str] = None, |
| ) -> SolverResult: |
| |
| return SolverResult( |
| domain="quant", |
| solved=solved, |
| topic=topic, |
| answer_value=answer_value if answer_value is not None else internal_answer, |
| answer_letter=answer_letter, |
| internal_answer=internal_answer, |
| steps=steps, |
| ) |
|
|
|
|
| def _mentions_remainders(lower: str) -> bool: |
| triggers = [ |
| "remainder", |
| "mod ", |
| " mod", |
| "divisible", |
| "divided by", |
| "leaves", |
| "left over", |
| "last digit", |
| "units digit", |
| "tens digit", |
| "ones digit", |
| ] |
| return any(t in lower for t in triggers) |
|
|
|
|
| |
| |
| |
|
|
| def _solve_direct_numeric_remainder(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| m = re.search( |
| r"remainder.*?when\s+(.+?)\s+(?:is\s+)?divided\s+by\s+(-?\d+)", |
| lower, |
| ) |
| if m: |
| expr = m.group(1).strip() |
| mod = int(m.group(2)) |
| val = _safe_int_eval(expr) |
| if val is not None and mod != 0: |
| result = val % mod |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Write the dividend in quotient–remainder form: dividend = divisor × quotient + remainder.", |
| "Reduce the computed value modulo the divisor.", |
| "Keep the remainder nonnegative and smaller than the divisor.", |
| ], |
| ) |
|
|
| |
| m = re.search(r"(-?\d+(?:\s*[\+\-\*\/%]\s*-?\d+|\s*\*\*\s*-?\d+)*)\s*(?:mod|%)\s*(-?\d+)", lower) |
| if m: |
| expr = m.group(1).strip() |
| mod = int(m.group(2)) |
| val = _safe_int_eval(expr) |
| if val is not None and mod != 0: |
| result = val % mod |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Interpret the expression as a modulo calculation.", |
| "Evaluate the expression carefully, then reduce modulo the divisor.", |
| "Adjust to the standard remainder range from 0 up to divisor - 1.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_last_digit_patterns(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| m = re.search(r"(?:last|ones|units)\s+digit\s+of\s+(.+)", lower) |
| if m: |
| expr = m.group(1).strip(" ?.") |
| val = _safe_int_eval(expr) |
| if val is not None: |
| result = val % 10 |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "The last digit is the remainder upon division by 10.", |
| "So reduce the number modulo 10 rather than working with the entire value.", |
| "That gives the required digit.", |
| ], |
| topic="remainder", |
| ) |
|
|
| |
| |
| m = re.search(r"remainder.*?divided by 100.*?is\s+(\d+)", lower) |
| if m and ("tens digit" in lower): |
| rem = int(m.group(1)) |
| tens = (rem // 10) % 10 |
| return _make_result( |
| internal_answer=str(tens), |
| steps=[ |
| "A remainder upon division by 100 preserves the last two digits.", |
| "The tens digit is therefore the tens digit of that two-digit remainder.", |
| "Read the target digit directly from the remainder.", |
| ], |
| topic="remainder", |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_known_remainder_linear_transform(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| m = re.search( |
| r"remainder\s+(?:is|of)\s+(\d+)\s+when\s+\w+\s+is\s+divided\s+by\s+(\d+).*?" |
| r"what\s+is\s+the\s+remainder\s+when\s+(\d+)\s*\*?\s*\w+\s+is\s+divided\s+by\s+(\d+)", |
| lower, |
| ) |
| if m: |
| r = int(m.group(1)) |
| old_mod = int(m.group(2)) |
| mult = int(m.group(3)) |
| new_mod = int(m.group(4)) |
| |
| if old_mod % new_mod == 0: |
| result = (mult * r) % new_mod |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Replace the variable by divisor × quotient + known remainder.", |
| "Multiply through, then ignore the term guaranteed to stay divisible by the new divisor.", |
| "Reduce the remaining expression modulo the requested divisor.", |
| ], |
| ) |
|
|
| |
| m = re.search( |
| r"(?:(?:when|if)\s+)?(?:positive\s+integer\s+)?([a-z])\s+(?:is\s+)?divided\s+by\s+(\d+)\s+" |
| r"(?:has|leaves|gives)\s+(?:a\s+)?remainder\s+(?:of\s+)?(\d+).*?" |
| 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+)", |
| lower, |
| ) |
| if m: |
| mod1 = int(m.group(2)) |
| r = int(m.group(3)) |
| a_txt = m.group(4).strip() |
| b_txt = (m.group(5) or "").replace(" ", "") |
| mod2 = int(m.group(6)) |
|
|
| if a_txt in ("", "+"): |
| a = 1 |
| elif a_txt == "-": |
| a = -1 |
| else: |
| a = int(a_txt) |
|
|
| b = int(b_txt) if b_txt else 0 |
|
|
| if mod1 == mod2: |
| result = (a * r + b) % mod2 |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Substitute the variable with its remainder class modulo the divisor.", |
| "Apply the linear transformation to the remainder instead of to the full number.", |
| "Reduce once more to keep the answer in the standard remainder range.", |
| ], |
| ) |
|
|
| |
| m = re.search( |
| r"remainder\s+(?:is|of)\s+(\d+)\s+when\s+([a-z])\s+is\s+divided\s+by\s+(\d+).*?" |
| r"what\s+is\s+the\s+remainder\s+when\s+\2\s+is\s+divided\s+by\s+(\d+)", |
| lower, |
| ) |
| if m: |
| r = int(m.group(1)) |
| big_mod = int(m.group(3)) |
| small_mod = int(m.group(4)) |
| if big_mod % small_mod == 0: |
| result = r % small_mod |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Write the number as big divisor × quotient + known remainder.", |
| "If the big divisor is a multiple of the smaller divisor, that first term contributes no new remainder.", |
| "So reduce the known remainder by the smaller divisor.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_power_remainder(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| m = re.search( |
| r"when\s+(-?\d+)\s*\*\*\s*(\d+)\s+is\s+divided\s+by\s+(\d+)", lower |
| ) |
| if not m: |
| m = re.search( |
| r"when\s+(-?\d+)\s*\^\s*(\d+)\s+is\s+divided\s+by\s+(\d+)", text.lower() |
| ) |
| if not m: |
| m = re.search( |
| r"remainder.*?when\s+(-?\d+)\s*(?:\^|\*\*)\s*(\d+)\s+(?:is\s+)?divided\s+by\s+(\d+)", |
| lower, |
| ) |
|
|
| if m: |
| base = int(m.group(1)) |
| exp = int(m.group(2)) |
| mod = int(m.group(3)) |
| if mod != 0: |
| result = pow(base, exp, mod) |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Reduce the base modulo the divisor first.", |
| "Then use modular exponentiation or a repeating cycle pattern.", |
| "Only the remainder class matters, not the full power.", |
| ], |
| ) |
|
|
| |
| 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: |
| result = 1 |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "A prime greater than 3 is not divisible by 2 or 3.", |
| "So it must be congruent to 1, 5, 7, or 11 modulo 12.", |
| "Squaring any of those residue classes gives the same remainder modulo 12.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_two_congruences_same_variable(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| |
| matches = re.findall( |
| r"([a-z])\s+(?:is\s+)?(?:greater\s+than\s+\d+\s+and\s+)?(?:leaves|has|gives)\s+(?:a\s+)?remainder\s+(\d+)\s+" |
| r"(?:after\s+division\s+by|when\s+divided\s+by)\s+(\d+)", |
| lower, |
| ) |
|
|
| if len(matches) >= 2: |
| v1, r1, m1 = matches[0] |
| v2, r2, m2 = matches[1] |
| if v1 == v2: |
| r1, m1, r2, m2 = int(r1), int(m1), int(r2), int(m2) |
| first = _first_crt_solution(r1, m1, r2, m2) |
| if first is not None: |
| l = _lcm(m1, m2) |
|
|
| |
| q = re.search( |
| rf"what\s+is\s+the\s+remainder.*?{v1}.*?divided\s+by\s+(\d+)", |
| lower, |
| ) |
| if q: |
| target_mod = int(q.group(1)) |
| result = first % target_mod |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Translate each condition into a congruence.", |
| "Find the first common value that satisfies both patterns, then express all solutions using the least common multiple of the divisors.", |
| "Reduce that shared residue class by the requested divisor.", |
| ], |
| ) |
|
|
| |
| result = first % l |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Find the overlap of the two arithmetic patterns.", |
| "That overlap becomes the residue class modulo the least common multiple of the divisors.", |
| "The remainder on division by that common modulus is the first shared residue.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_difference_same_remainders(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| m1 = re.search( |
| r"when\s+positive\s+integer\s+x\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+).*?" |
| r"when\s+x\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+)", |
| lower, |
| re.DOTALL, |
| ) |
| m2 = re.search( |
| r"when\s+positive\s+integer\s+y\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+).*?" |
| r"when\s+y\s+is\s+divided\s+by\s+(\d+),\s+the\s+remainder\s+is\s+(\d+)", |
| lower, |
| re.DOTALL, |
| ) |
|
|
| if m1 and m2: |
| ax1, ar1, ax2, ar2 = map(int, m1.groups()) |
| ay1, br1, ay2, br2 = map(int, m2.groups()) |
|
|
| if ax1 == ay1 and ax2 == ay2 and ar1 == br1 and ar2 == br2: |
| must_factor = _lcm(ax1, ax2) |
| return _make_result( |
| internal_answer=str(must_factor), |
| steps=[ |
| "If two numbers leave the same remainder upon division by a modulus, their difference is divisible by that modulus.", |
| "Apply that idea to each divisor separately.", |
| "So the difference must be divisible by the least common multiple of those divisors.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_decimal_quotient_remainder(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| m = re.search( |
| r"([a-z])\s*/\s*([a-z])\s*=\s*(\d+)\.(\d+).*?remainder\s+when\s+\1\s+is\s+divided\s+by\s+\2", |
| lower, |
| ) |
| if m: |
| |
| |
| frac_num = int(m.group(4)) |
| frac_den = 10 ** len(m.group(4)) |
| g = _gcd(frac_num, frac_den) |
| num = frac_num // g |
| |
| return _make_result( |
| internal_answer=str(num), |
| steps=[ |
| "Use dividend = divisor × quotient + remainder, then divide both sides by the divisor.", |
| "The decimal part of the quotient equals remainder/divisor.", |
| "Reduce the fractional part to lowest terms; the numerator controls the remainder pattern.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_divisibility_from_remainder_expression(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| |
| 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: |
| return _make_result( |
| internal_answer="yes", |
| steps=[ |
| "Factor the expression into consecutive integers.", |
| "Among consecutive integers, use parity structure to identify enough factors of 2.", |
| "That guarantees divisibility by the target power of 2.", |
| ], |
| topic="remainder", |
| answer_value="yes", |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_smaller_dividend_than_divisor(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| m = re.search( |
| r"remainder.*?when\s+(\d+)\s+(?:is\s+)?divided\s+by\s+(\d+)", |
| lower, |
| ) |
| if m: |
| a = int(m.group(1)) |
| b = int(m.group(2)) |
| if 0 <= a < b: |
| return _make_result( |
| internal_answer=str(a), |
| steps=[ |
| "If the dividend is smaller than the divisor, the quotient is 0.", |
| "So the entire dividend becomes the remainder.", |
| "No further reduction is needed.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| |
| |
| |
|
|
| def _solve_generic_known_remainder_question(text: str) -> Optional[SolverResult]: |
| lower = text.lower() |
|
|
| |
| |
| known = re.findall( |
| r"([a-z])\s+(?:is\s+)?divided\s+by\s+(\d+)\s+(?:has|gives|leaves)\s+(?:a\s+)?remainder\s+(?:of\s+)?(\d+)", |
| lower, |
| ) |
| if not known: |
| known = re.findall( |
| r"remainder\s+(?:is|of)\s+(\d+)\s+when\s+([a-z])\s+is\s+divided\s+by\s+(\d+)", |
| lower, |
| ) |
| known = [(v, m, r) for (r, v, m) in known] |
|
|
| if not known: |
| return None |
|
|
| var, mod, rem = known[0] |
| mod = int(mod) |
| rem = int(rem) |
|
|
| |
| q = re.search(rf"what\s+is\s+the\s+remainder\s+when\s+{var}\s+is\s+divided\s+by\s+(\d+)", lower) |
| if q: |
| target = int(q.group(1)) |
| if mod % target == 0: |
| result = rem % target |
| return _make_result( |
| internal_answer=str(result), |
| steps=[ |
| "Rewrite the number as divisor × quotient + remainder.", |
| "Check whether the known divisor is a multiple of the target divisor.", |
| "Then reduce the known remainder by the target divisor.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| |
| |
| |
|
|
| def solve_remainder(text: str) -> Optional[SolverResult]: |
| raw = text or "" |
| cleaned = _clean(raw) |
| lower = cleaned.lower() |
|
|
| if not _mentions_remainders(lower): |
| return None |
|
|
| solvers = [ |
| _solve_direct_numeric_remainder, |
| _solve_smaller_dividend_than_divisor, |
| _solve_last_digit_patterns, |
| _solve_known_remainder_linear_transform, |
| _solve_power_remainder, |
| _solve_two_congruences_same_variable, |
| _solve_difference_same_remainders, |
| _solve_decimal_quotient_remainder, |
| _solve_divisibility_from_remainder_expression, |
| _solve_generic_known_remainder_question, |
| ] |
|
|
| for fn in solvers: |
| try: |
| out = fn(cleaned) |
| if out is not None: |
| return out |
| except Exception: |
| continue |
|
|
| return None |