| from __future__ import annotations |
|
|
| import math |
| import re |
| from typing import Optional, List |
|
|
| from models import SolverResult |
|
|
|
|
| |
| |
| |
|
|
| def _clean(text: str) -> str: |
| return " ".join((text or "").strip().split()) |
|
|
|
|
| def _has_factorial_signal(raw: str, lower: str) -> bool: |
| return any( |
| [ |
| re.search(r"\b\d+\s*!", raw) is not None, |
| "factorial" in lower, |
| "trailing zero" in lower, |
| "trailing zeros" in lower, |
| "zeros at the end" in lower, |
| "power of" in lower and "!" in raw, |
| "highest power" in lower and "!" in raw, |
| "divide" in lower and "!" in raw, |
| ] |
| ) |
|
|
|
|
| def _is_prime(n: int) -> bool: |
| if n < 2: |
| return False |
| if n in (2, 3): |
| return True |
| if n % 2 == 0 or n % 3 == 0: |
| return False |
| f = 5 |
| while f * f <= n: |
| if n % f == 0 or n % (f + 2) == 0: |
| return False |
| f += 6 |
| return True |
|
|
|
|
| def _valuation_in_factorial(n: int, p: int) -> int: |
| """ |
| Exponent of prime p in n! using Legendre's formula: |
| floor(n/p) + floor(n/p^2) + ... |
| """ |
| total = 0 |
| power = p |
| while power <= n: |
| total += n // power |
| power *= p |
| return total |
|
|
|
|
| def _factorial_steps() -> List[str]: |
| return [ |
| "A factorial means multiply consecutive positive integers down to 1.", |
| "Rewrite the factorial in expanded form if needed.", |
| "Then simplify carefully, often by canceling common factors first.", |
| ] |
|
|
|
|
| def _trailing_zero_steps() -> List[str]: |
| return [ |
| "Trailing zeros come from factors of 10.", |
| "Each factor of 10 is made from one 2 and one 5.", |
| "In a factorial, there are usually more 2s than 5s, so the limiting factor is the number of 5s.", |
| "Count factors of 5 using multiples of 5, then add extra 5s from 25, 125, and so on.", |
| ] |
|
|
|
|
| def _prime_power_steps(p: int) -> List[str]: |
| return [ |
| f"To find the power of {p} in n!, count how many times {p} appears in the factorial product.", |
| f"Use repeated floor division: floor(n/{p}) + floor(n/{p**2}) + floor(n/{p**3}) + ...", |
| "Stop when the next power of the prime is larger than n.", |
| "That total gives the exponent of the prime in the factorial.", |
| ] |
|
|
|
|
| def _ratio_steps() -> List[str]: |
| return [ |
| "Expand only the larger factorial until cancellation is possible.", |
| "Do not expand both factorials fully unless the numbers are very small.", |
| "Cancel the common descending product.", |
| "Then simplify the remaining factors.", |
| ] |
|
|
|
|
| def _equation_steps() -> List[str]: |
| return [ |
| "Use factorial growth: 1!, 2!, 3!, 4!, ... gets large quickly.", |
| "Compare nearby factorial values rather than expanding everything at once.", |
| "Match the factorial expression to the target value or simplified form.", |
| ] |
|
|
|
|
| def _normalize_factorial_expr(expr: str) -> str: |
| expr = expr.replace("^", "**") |
| expr = expr.replace("×", "*") |
| expr = expr.replace("÷", "/") |
| expr = expr.replace("–", "-") |
| expr = expr.replace("—", "-") |
| expr = expr.strip() |
| return expr |
|
|
|
|
| def _factorial_to_python(expr: str) -> str: |
| """ |
| Convert occurrences like 7! into math.factorial(7). |
| Only supports numeric factorial arguments. |
| """ |
| expr = _normalize_factorial_expr(expr) |
|
|
| def repl(match: re.Match) -> str: |
| num = match.group(1) |
| return f"math.factorial({num})" |
|
|
| return re.sub(r"(\d+)\s*!", repl, expr) |
|
|
|
|
| def _safe_eval_numeric_factorial_expr(expr: str) -> Optional[int]: |
| """ |
| Evaluate a numeric factorial expression like: |
| 8!/5! |
| (7!*3!)/(6!) |
| Only for controlled numeric inputs after conversion. |
| """ |
| try: |
| py_expr = _factorial_to_python(expr) |
| if not re.fullmatch(r"[0-9\(\)\s\+\-\*/,.a-zA-Z_]+", py_expr): |
| return None |
| value = eval(py_expr, {"__builtins__": {}}, {"math": math}) |
| if isinstance(value, (int, float)) and abs(value - round(value)) < 1e-9: |
| return int(round(value)) |
| return None |
| except Exception: |
| return None |
|
|
|
|
| def _extract_smallest_positive_solution_for_factorial_value(target: int) -> Optional[int]: |
| """ |
| Solve n! = target for integer n if possible. |
| """ |
| if target < 1: |
| return None |
| n = 1 |
| fact = 1 |
| while fact < target and n <= 50: |
| n += 1 |
| fact *= n |
| if fact == target: |
| return n |
| return None |
|
|
|
|
| def _extract_numbers_from_text(lower: str) -> List[int]: |
| return [int(x) for x in re.findall(r"\b\d+\b", lower)] |
|
|
|
|
| |
| |
| |
|
|
| def solve_factorial(text: str) -> Optional[SolverResult]: |
| raw = text or "" |
| lower = _clean(raw).lower() |
|
|
| if not raw.strip(): |
| return None |
|
|
| if not _has_factorial_signal(raw, lower): |
| return None |
|
|
| |
| |
| |
| |
| |
| |
| |
| trailing_patterns = [ |
| r"trailing zeros?.*?(?:in|of)?\s*(\d+)\s*!", |
| r"zeros? at the end.*?(?:in|of)?\s*(\d+)\s*!", |
| r"how many zeros?.*?(?:end|trailing).*?(\d+)\s*!", |
| r"(\d+)\s*!\s*.*?trailing zeros?", |
| r"(\d+)\s*factorial.*?trailing zeros?", |
| r"zeros? at the end of\s*(\d+)\s*factorial", |
| ] |
| for pattern in trailing_patterns: |
| m = re.search(pattern, lower) |
| if m: |
| n = int(m.group(1)) |
| if n < 0: |
| return None |
|
|
| count = _valuation_in_factorial(n, 5) |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_trailing_zeros", |
| answer_value=None, |
| internal_answer=str(count), |
| steps=_trailing_zero_steps(), |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| prime_power_patterns = [ |
| r"power of\s+(\d+)\s+in\s+(\d+)\s*!", |
| r"exponent of\s+(\d+)\s+in\s+(\d+)\s*!", |
| r"highest power of\s+(\d+)\s+(?:that\s+)?divides\s+(\d+)\s*!", |
| r"how many times does\s+(\d+)\s+divide\s+(\d+)\s*!", |
| r"factor of\s+(\d+)\s+in\s+(\d+)\s*!", |
| ] |
| for pattern in prime_power_patterns: |
| m = re.search(pattern, lower) |
| if m: |
| p = int(m.group(1)) |
| n = int(m.group(2)) |
|
|
| if n < 0 or not _is_prime(p): |
| return None |
|
|
| count = _valuation_in_factorial(n, p) |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_prime_power", |
| answer_value=None, |
| internal_answer=str(count), |
| steps=_prime_power_steps(p), |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| numeric_factorials = re.findall(r"\d+\s*!", raw) |
| if numeric_factorials: |
| factorial_count = len(numeric_factorials) |
| has_arithmetic = any(sym in raw for sym in ["/", "*", "+", "-", "(", ")"]) |
|
|
| if factorial_count >= 2 or (factorial_count >= 1 and has_arithmetic): |
| expr_candidate = raw.strip() |
|
|
| |
| expr_match = re.search(r"([\d\s!\(\)\+\-\*/×÷^]+)", raw) |
| if expr_match: |
| maybe_expr = expr_match.group(1).strip() |
| if "!" in maybe_expr: |
| expr_candidate = maybe_expr |
|
|
| value = _safe_eval_numeric_factorial_expr(expr_candidate) |
| if value is not None: |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_expression", |
| answer_value=None, |
| internal_answer=str(value), |
| steps=_ratio_steps(), |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| eq_match = re.search( |
| r"(?:n|x)\s*!\s*=\s*(\d+)|(?:which value of|find)\s+(?:n|x).*?(?:n|x)\s*!\s*=\s*(\d+)", |
| lower |
| ) |
| if eq_match: |
| target = eq_match.group(1) or eq_match.group(2) |
| if target is not None: |
| target_int = int(target) |
| sol = _extract_smallest_positive_solution_for_factorial_value(target_int) |
| if sol is not None: |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_equation", |
| answer_value=None, |
| internal_answer=str(sol), |
| steps=_equation_steps(), |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| direct_patterns = [ |
| r"^\s*(\d+)\s*!\s*$", |
| r"value of\s+(\d+)\s*!", |
| r"what is\s+(\d+)\s*!", |
| r"factorial of\s+(\d+)", |
| r"value of\s+(\d+)\s+factorial", |
| ] |
| for pattern in direct_patterns: |
| m = re.search(pattern, lower) |
| if m: |
| n = int(m.group(1)) |
| if n < 0: |
| return None |
|
|
| result = math.factorial(n) |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial", |
| answer_value=None, |
| internal_answer=str(result), |
| steps=_factorial_steps(), |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| units_match = re.search(r"units digit.*?(\d+)\s*!|(\d+)\s*!\s*.*?units digit", lower) |
| if units_match: |
| n_str = units_match.group(1) or units_match.group(2) |
| if n_str is not None: |
| n = int(n_str) |
| value = math.factorial(n) |
| units = value % 10 |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_units_digit", |
| answer_value=None, |
| internal_answer=str(units), |
| steps=[ |
| "Compute or reason about the final digit of the factorial product.", |
| "For factorials with n ≥ 5, a factor of 10 appears, so the units digit becomes 0.", |
| "For smaller factorials, expand directly.", |
| ], |
| ) |
|
|
| last_nonzero_match = re.search( |
| r"last non-?zero digit.*?(\d+)\s*!|(\d+)\s*!\s*.*?last non-?zero digit", |
| lower, |
| ) |
| if last_nonzero_match: |
| n_str = last_nonzero_match.group(1) or last_nonzero_match.group(2) |
| if n_str is not None: |
| n = int(n_str) |
| value = math.factorial(n) |
| while value % 10 == 0: |
| value //= 10 |
| last_nonzero = value % 10 |
|
|
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_last_nonzero_digit", |
| answer_value=None, |
| internal_answer=str(last_nonzero), |
| steps=[ |
| "Find the factorial value or reason from its prime-factor structure.", |
| "Remove trailing zeros first.", |
| "Then take the final remaining digit.", |
| ], |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| power_divides_match = re.search( |
| r"greatest integer\s+(?:k|n).*?(\d+)\s*\^\s*(?:k|n)\s+divides\s+(\d+)\s*!", |
| lower |
| ) |
| if power_divides_match: |
| base = int(power_divides_match.group(1)) |
| n = int(power_divides_match.group(2)) |
| if _is_prime(base): |
| count = _valuation_in_factorial(n, base) |
| return SolverResult( |
| domain="quant", |
| solved=True, |
| topic="factorial_prime_power", |
| answer_value=None, |
| internal_answer=str(count), |
| steps=_prime_power_steps(base), |
| ) |
|
|
| |
| |
| |
| |
| |
| if "!" in raw or "factorial" in lower: |
| return SolverResult( |
| domain="quant", |
| solved=False, |
| topic="factorial", |
| answer_value=None, |
| internal_answer=None, |
| steps=[ |
| "Identify whether the question is asking for direct evaluation, cancellation, trailing zeros, or prime-factor counting.", |
| "If it is a ratio, expand only enough terms to cancel.", |
| "If it is about zeros or divisibility, convert the question into counting prime factors inside the factorial.", |
| "Then simplify step by step without expanding more than necessary.", |
| ], |
| ) |
|
|
| return None |