89 lines
3.5 KiB
JavaScript
89 lines
3.5 KiB
JavaScript
'use strict';
|
||
/* ──────────────────────────────────────────────────────────────────
|
||
Answer normalization + correctness check.
|
||
Same algorithm runs on user input and stored canonical answer
|
||
(from /tasks endpoint), then compared.
|
||
|
||
Canonical answer forms (produced by backend/scripts/import-exam-tasks.js):
|
||
MC: single Cyrillic letter: "а" | "б" | "в" | "г" | "д"
|
||
open NUM: decimal: "-2" "7500" "1.5" "0.25"
|
||
open FRAC: fraction: "9/4" "-104/9" (sign on numerator)
|
||
open PAIR: two values, ";" sep: "-2;4"
|
||
The user can type the answer in many equivalent forms — the normalizer
|
||
accepts any form and decides equivalence numerically.
|
||
────────────────────────────────────────────────────────────────── */
|
||
|
||
(function () {
|
||
const EPS = 1e-6;
|
||
|
||
// Try to coerce a string to a number; returns NaN on failure.
|
||
function toNumber(s) {
|
||
if (s == null) return NaN;
|
||
let t = String(s).trim().replace(/\$/g, '').replace(/\s+/g, '').replace(',', '.');
|
||
// Fraction "a/b" → a/b
|
||
const f = t.match(/^(-?\d+(?:\.\d+)?)\s*\/\s*(-?\d+(?:\.\d+)?)$/);
|
||
if (f) {
|
||
const num = Number(f[1]);
|
||
const den = Number(f[2]);
|
||
if (!Number.isFinite(num) || !Number.isFinite(den) || den === 0) return NaN;
|
||
return num / den;
|
||
}
|
||
const n = Number(t);
|
||
return Number.isFinite(n) ? n : NaN;
|
||
}
|
||
|
||
// Parse a "pair" answer "A;B" — accepts ";" or "," or " и " or whitespace
|
||
function toPair(s) {
|
||
if (s == null) return null;
|
||
const t = String(s).trim().replace(/\$/g, '').replace(/\s+и\s+/g, ';');
|
||
const parts = t.split(/[;,]/).map(p => p.trim()).filter(Boolean);
|
||
if (parts.length !== 2) return null;
|
||
const a = toNumber(parts[0]);
|
||
const b = toNumber(parts[1]);
|
||
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
||
// Order-insensitive comparison: return sorted pair
|
||
return a <= b ? [a, b] : [b, a];
|
||
}
|
||
|
||
/* Detect form of the canonical answer.
|
||
Returns: 'mc' | 'pair' | 'numeric' */
|
||
function detectForm(canonical) {
|
||
if (canonical == null) return 'numeric';
|
||
const t = String(canonical).trim();
|
||
if (/^[а-д]$/.test(t)) return 'mc';
|
||
if (/^[^;]+;[^;]+$/.test(t)) return 'pair';
|
||
return 'numeric';
|
||
}
|
||
|
||
/* Main check. Returns true iff user input matches canonical answer.
|
||
False if either side can't be normalized.
|
||
|
||
Both inputs are strings as returned by the form or stored in DB. */
|
||
function check(userInput, canonical) {
|
||
if (userInput == null || canonical == null) return false;
|
||
|
||
const form = detectForm(canonical);
|
||
|
||
if (form === 'mc') {
|
||
const u = String(userInput).trim().toLowerCase();
|
||
return u === String(canonical).trim().toLowerCase();
|
||
}
|
||
|
||
if (form === 'pair') {
|
||
const cp = toPair(canonical);
|
||
const up = toPair(userInput);
|
||
if (!cp || !up) return false;
|
||
return Math.abs(cp[0] - up[0]) < EPS && Math.abs(cp[1] - up[1]) < EPS;
|
||
}
|
||
|
||
// Numeric (integer / decimal / fraction)
|
||
const c = toNumber(canonical);
|
||
const u = toNumber(userInput);
|
||
if (Number.isNaN(c) || Number.isNaN(u)) return false;
|
||
return Math.abs(c - u) < EPS;
|
||
}
|
||
|
||
window.EP = window.EP || {};
|
||
window.EP.answer = { check, detectForm, toNumber, toPair };
|
||
})();
|