Spaces:
Paused
Paused
File size: 3,463 Bytes
917e554 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | """
Deterministic arithmetic checker for student working steps.
Uses sympy for safe expression evaluation.
"""
import re
from typing import List, Dict, Optional
from sympy import sympify, SympifyError, N
def _preprocess_expression(expr: str) -> str:
"""Convert common math notation to sympy-compatible."""
expr = expr.strip()
# Replace multiplication signs
expr = expr.replace('×', '*').replace('·', '*')
# Replace division signs
expr = expr.replace('÷', '/')
# Handle "of" as multiplication (e.g., "20% of 100")
expr = re.sub(r'\bof\b', '*', expr, flags=re.IGNORECASE)
# Handle percent
expr = re.sub(r'(\d+(?:\.\d+)?)\s*%', r'(\1/100)', expr)
# Handle implicit multiplication (e.g., "2x" -> "2*x")
expr = re.sub(r'(\d)([a-zA-Z])', r'\1*\2', expr)
# Handle spaces as multiplication between number and variable/paren
expr = re.sub(r'(\d)\s+([a-zA-Z\(])', r'\1*\2', expr)
return expr
def check_working(working_text: str) -> List[Dict[str, any]]:
"""
Check each equation line in student working.
Returns list of {line, correct, expected} for each line.
"""
results = []
# Split on newlines, semicolons, commas (but not inside numbers)
# First, normalize line breaks
text = working_text.replace(';', '\n').replace(',', '\n')
lines = [line.strip() for line in text.split('\n') if line.strip()]
for line in lines:
# Check if line looks like an equation with =
if '=' not in line:
results.append({
"line": line,
"correct": None,
"expected": None,
"reason": "Not an equation"
})
continue
parts = line.split('=')
if len(parts) != 2:
results.append({
"line": line,
"correct": None,
"expected": None,
"reason": "Multiple equals signs"
})
continue
lhs = parts[0].strip()
rhs = parts[1].strip()
# Try to evaluate both sides
try:
lhs_processed = _preprocess_expression(lhs)
rhs_processed = _preprocess_expression(rhs)
lhs_val = sympify(lhs_processed, evaluate=True)
rhs_val = sympify(rhs_processed, evaluate=True)
# Check if both are numeric
if lhs_val.is_number and rhs_val.is_number:
lhs_float = float(N(lhs_val))
rhs_float = float(N(rhs_val))
correct = abs(lhs_float - rhs_float) < 1e-6
expected = str(rhs_float) if not correct else None
results.append({
"line": line,
"correct": correct,
"expected": expected,
"reason": "Numeric mismatch" if not correct else "OK"
})
else:
# Non-numeric (has variables) - can't verify
results.append({
"line": line,
"correct": None,
"expected": None,
"reason": "Contains variables"
})
except (SympifyError, TypeError, ValueError, ZeroDivisionError) as e:
results.append({
"line": line,
"correct": None,
"expected": None,
"reason": f"Parse error: {str(e)}"
})
return results |