Gateprep / backend /app /services /answer_utils.py
banu4prasad's picture
code split: admin.py
049ecad
Raw
History Blame Contribute Delete
3.18 kB
import math
import re
from typing import Any, Optional
_NUMBER = r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?"
_NAT_RANGE_RE = re.compile(
rf"^\s*({_NUMBER})\s*(?:-|:|to)\s*({_NUMBER})\s*$", re.IGNORECASE
)
_ANSWER_SPLIT_RE = re.compile(r"\s*[,;/]\s*")
def normalize_question_type(question_type: Any) -> str:
raw = getattr(question_type, "value", question_type)
value = str(raw or "").strip().lower()
if "." in value:
value = value.rsplit(".", 1)[-1]
return value
def parse_float(value: Any) -> Optional[float]:
try:
parsed = float(str(value).strip())
except (TypeError, ValueError):
return None
return parsed if math.isfinite(parsed) else None
def parse_nat_range(value: Any) -> Optional[tuple[float, float]]:
text = str(value or "").strip().replace(" ", "")
match = _NAT_RANGE_RE.match(text)
if not match:
return None
lo = parse_float(match.group(1))
hi = parse_float(match.group(2))
if lo is None or hi is None or lo > hi:
return None
return lo, hi
def split_answer_tokens(value: Any) -> list[str]:
text = str(value or "").strip()
return [part.strip() for part in _ANSWER_SPLIT_RE.split(text) if part.strip()]
def is_valid_nat_answer(value: Any) -> bool:
tokens = split_answer_tokens(value)
if not tokens:
return False
return all(
parse_float(token.replace(" ", "")) is not None
or parse_nat_range(token.replace(" ", "")) is not None
for token in tokens
)
def normalize_choice_answer(value: Any) -> str:
return ",".join(token.strip().upper() for token in split_answer_tokens(value))
def validate_mcq_answer(answer: str) -> str:
"""Validate and return a normalised MCQ answer."""
if answer not in {"A", "B", "C", "D"}:
raise ValueError("MCQ correct_answer must be one of A, B, C, or D")
return answer
def validate_msq_answer(answer: str) -> tuple[str, float]:
"""Validate an MSQ answer and return (normalised_answer, negative_marks)."""
selected = [part.strip().upper() for part in split_answer_tokens(answer)]
if not selected or any(part not in {"A", "B", "C", "D"} for part in selected):
raise ValueError("MSQ correct_answer must contain option letters like A,C")
return ",".join(dict.fromkeys(selected)), 0.0
def validate_nat_answer(answer: str) -> float:
"""Validate a NAT answer and return the forced negative_marks value."""
if not is_valid_nat_answer(answer):
raise ValueError(
"NAT correct_answer must be a number or range like 41.5-42.5 or 41.5:42.5"
)
return 0.0
def validate_answer_for_type(
q_type: str, correct_answer: str
) -> tuple[str, float | None, bool]:
"""Validate correct_answer against q_type.
Returns (normalised_answer, override_negative_marks_or_None, clear_options).
"""
if q_type == "mcq":
return validate_mcq_answer(correct_answer), None, False
if q_type == "msq":
answer, neg = validate_msq_answer(correct_answer)
return answer, neg, False
# nat
neg = validate_nat_answer(correct_answer)
return correct_answer, neg, True