fix(ct-math): варианты ответа из текста → нормальный opts_json (mc ctmath)
У части mc-задач ЦТ (формат РИКЗ «укажите номер») список ответов был вшит в текст («1) 44; 2) 22; …»), а opts содержали лишь цифры-указатели — рисовалось «а) 1, б) 2…» + значения строкой. Скрипт fix_ctmath_inline_opts.js вытаскивает список из текста в opts_json (метка=цифра, текст=значение), пересчитывает answer, очищает текст. Последовательный парсер сохраняет ';' внутри значений (интервалы). Dry: 281 кандидат → 213 чинятся чисто, 68 нестандартных пропущены (без порчи). Запись (UPDATE 213) — запускает пользователь (--apply), как и прочие записи в БД. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
'use strict';
|
||||||
|
/*
|
||||||
|
* Фикс mc-задач ctmath, где варианты ответа вшиты в текст («1) 44; 2) 22; …»),
|
||||||
|
* а opts_json содержит лишь цифры-указатели. Вытаскивает список из текста в
|
||||||
|
* нормальный opts_json (метка=цифра, текст=значение), пересчитывает answer,
|
||||||
|
* очищает текст. Только для чисто распознанных случаев (иначе пропуск).
|
||||||
|
* node backend/scripts/fix_ctmath_inline_opts.js # dry: статистика+выборка
|
||||||
|
* node backend/scripts/fix_ctmath_inline_opts.js --apply # запись (UPDATE)
|
||||||
|
*/
|
||||||
|
const db = require('../src/db/db');
|
||||||
|
const APPLY = process.argv.includes('--apply');
|
||||||
|
|
||||||
|
// Разбор инлайн-списка "1) v1; 2) v2; … N) vN."
|
||||||
|
// Последовательный: режем значение только по " ; (n+1)) " следующего номера,
|
||||||
|
// поэтому ';' внутри значений (интервалы вида (-6;9)) сохраняются.
|
||||||
|
function parseInline(text) {
|
||||||
|
const m1 = text.match(/(^|[\s:>(])1\)\s/);
|
||||||
|
if (!m1) return null;
|
||||||
|
const start = m1.index + m1[1].length; // позиция "1)"
|
||||||
|
const stem = text.slice(0, start).replace(/[\s:]+$/, '').trim();
|
||||||
|
if (!stem) return null;
|
||||||
|
let rest = text.slice(start);
|
||||||
|
const h1 = /^1\)\s*/;
|
||||||
|
if (!h1.test(rest)) return null;
|
||||||
|
rest = rest.replace(h1, ''); // "1)" снимаем один раз
|
||||||
|
const pairs = [];
|
||||||
|
let n = 1;
|
||||||
|
while (true) {
|
||||||
|
const nextRe = new RegExp('\\s*;?\\s*' + (n + 1) + '\\)\\s');
|
||||||
|
const nm = rest.match(nextRe);
|
||||||
|
let val;
|
||||||
|
if (nm) { val = rest.slice(0, nm.index); rest = rest.slice(nm.index + nm[0].length); }
|
||||||
|
else { val = rest; rest = ''; } // последний пункт
|
||||||
|
val = val.replace(/[;.\s]+$/, '').trim();
|
||||||
|
if (!val) return null;
|
||||||
|
pairs.push([String(n), val]);
|
||||||
|
if (!nm) break;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (pairs.length < 2) return null;
|
||||||
|
return { stem, pairs };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = db.prepare("SELECT id, text_html, opts_json, answer FROM exam_tasks WHERE exam_key='ctmath' AND task_type='mc'").all();
|
||||||
|
const stat = { total: rows.length, candidate: 0, fixed: 0, skip_notdigit: 0, skip_parse: 0, skip_count: 0, skip_answer: 0 };
|
||||||
|
const updates = [];
|
||||||
|
|
||||||
|
for (const r of rows) {
|
||||||
|
let opts; try { opts = JSON.parse(r.opts_json); } catch { continue; }
|
||||||
|
const texts = opts.map(p => String(p[1]).replace(/\$/g, '').trim());
|
||||||
|
const isDigitPtr = texts.length >= 2 && texts.every(x => /^[1-9][0-9]?$/.test(x));
|
||||||
|
if (!isDigitPtr) { stat.skip_notdigit++; continue; }
|
||||||
|
stat.candidate++;
|
||||||
|
|
||||||
|
const parsed = parseInline(r.text_html);
|
||||||
|
if (!parsed) { stat.skip_parse++; continue; }
|
||||||
|
if (parsed.pairs.length !== opts.length) { stat.skip_count++; continue; }
|
||||||
|
|
||||||
|
// correctDigit = указатель, на который ссылается текущий answer
|
||||||
|
const ai = opts.findIndex(p => String(p[0]).toLowerCase() === String(r.answer).toLowerCase());
|
||||||
|
const correctDigit = ai >= 0 ? String(opts[ai][1]).replace(/\$/g, '').trim() : null;
|
||||||
|
if (!correctDigit || !/^[1-9][0-9]?$/.test(correctDigit) || Number(correctDigit) > parsed.pairs.length) { stat.skip_answer++; continue; }
|
||||||
|
|
||||||
|
const newOpts = JSON.stringify(parsed.pairs); // [["1","44"],...]
|
||||||
|
updates.push({ id: r.id, text: parsed.stem, opts: newOpts, answer: correctDigit, _old: r.text_html, _newpairs: parsed.pairs });
|
||||||
|
stat.fixed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(APPLY ? '[APPLY]' : '[DRY-RUN]', 'mc всего', stat.total);
|
||||||
|
console.log('Статистика:', JSON.stringify(stat));
|
||||||
|
|
||||||
|
console.log('\n— Выборка (3) —');
|
||||||
|
for (const u of updates.slice(0, 3)) {
|
||||||
|
console.log(`\n id=${u.id}`);
|
||||||
|
console.log(' было text:', u._old.replace(/\s+/g, ' ').slice(0, 120));
|
||||||
|
console.log(' стало text:', u.text.replace(/\s+/g, ' ').slice(0, 90));
|
||||||
|
console.log(' стало opts:', u.opts.slice(0, 160), '| answer:', u.answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!APPLY) { console.log('\nDRY-RUN: запись НЕ выполнялась. Запись: --apply'); process.exit(0); }
|
||||||
|
|
||||||
|
const upd = db.prepare('UPDATE exam_tasks SET text_html=@text, opts_json=@opts, answer=@answer WHERE id=@id');
|
||||||
|
let n = 0;
|
||||||
|
for (const u of updates) { upd.run({ id: u.id, text: u.text, opts: u.opts, answer: u.answer }); n++; }
|
||||||
|
console.log(`\nОбновлено ${n} задач.`);
|
||||||
Reference in New Issue
Block a user