from __future__ import annotations import ast import math import re from typing import Optional, List, Tuple from models import SolverResult # ---------------------------- # Helpers # ---------------------------- 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: # keep the true answer internal; downstream formatter should avoid printing it directly 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) # ---------------------------- # Core patterns # ---------------------------- def _solve_direct_numeric_remainder(text: str) -> Optional[SolverResult]: lower = text.lower() # "remainder when 17 is divided by 5" 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.", ], ) # "123 mod 10" or "123 % 10" 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() # "last digit of 12345" / "ones digit of 12345" 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", ) # "tens digit of x" given remainder 30 when divided by 100 is harder DS style; # here support simple explicit numeric prompts only. 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() # n divided by m leaves remainder r; what remainder when k*n is divided by m? 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)) # only safe when divisor is same or the old divisor term remains divisible by new divisor 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.", ], ) # n leaves remainder r on division by m. what is remainder when (a*n + b) is divided by m? 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.", ], ) # book-style: remainder 7 when n divided by 18; find remainder when n divided by 6 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() # "when 51^25 is divided by 13" 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.", ], ) # special pattern: prime > 3, remainder when n^2 is divided by 12 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() # Example: # n leaves remainder 4 when divided by 6 and remainder 3 when divided by 5 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) # ask for remainder when same variable divided by lcm or another modulus 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.", ], ) # if question explicitly asks remainder on division by lcm 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() # x and y have same remainder mod 5 and mod 7; factor of x-y? 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() # s/t = 64.12 ; what could be the remainder when s is divided by t? 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: # If s/t = q + frac, then remainder / t = frac. # For 64.12 = 64 + 12/100 = 64 + 3/25, so remainder must be a multiple of 3. frac_num = int(m.group(4)) frac_den = 10 ** len(m.group(4)) g = _gcd(frac_num, frac_den) num = frac_num // g # remainder = num * k, t = den * k 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() # Is n divisible by d? / what is remainder when expression divided by d? # x^3 - x pattern 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 # ---------------------------- # Fallback pattern helpers # ---------------------------- def _solve_generic_known_remainder_question(text: str) -> Optional[SolverResult]: lower = text.lower() # Parse statements like: # "n divided by 5 has remainder 2" 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) # ask for variable itself divided by another modulus 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 # ---------------------------- # Main entry # ---------------------------- 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