const XLSX = require('xlsx'); const SUBJECT_NAMES = [ 'Русский язык', 'Математика', 'Обществознание', 'Физика', 'Химия', 'Информатика', 'Биология', 'Английский язык', 'География', 'Литература', 'История', 'Немецкий язык', 'Французский язык', 'Испанский язык' ]; const HEADER_MAP = { 'фамилия': 'last_name', 'имя': 'first_name', 'отчество': 'patronymic', 'класс': 'class', 'код оо': 'school_code', 'серия': 'passport_series', 'номер': 'passport_number', 'первичный балл': 'primary_score', 'оценка': 'grade' }; function normalize(s) { return String(s || '').trim().toLowerCase(); } function parseXls(filePath) { const workbook = XLSX.readFile(filePath); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' }); const parsedSubjects = []; const studentsMap = new Map(); const results = []; let currentSubject = null; let colIdx = null; for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row || row.every(c => c === '' || c === null || c === undefined)) continue; const firstCell = String(row[0] || '').trim(); if (isSummaryRowText(firstCell)) { currentSubject = null; colIdx = null; continue; } const subjectInfo = detectSubject(firstCell); if (subjectInfo) { currentSubject = subjectInfo; parsedSubjects.push(subjectInfo); colIdx = null; continue; } if (currentSubject && !colIdx) { colIdx = findColumnHeaders(row); if (colIdx) continue; } if (currentSubject && colIdx) { const sd = extractStudentData(row, colIdx); if (sd && sd.last_name) { const key = `${normalize(sd.last_name)}|${normalize(sd.first_name)}|${normalize(sd.patronymic)}|${normalize(sd.class)}`; if (!studentsMap.has(key)) { studentsMap.set(key, { last_name: sd.last_name, first_name: sd.first_name, patronymic: sd.patronymic, class: sd.class, school_code: sd.school_code || '', passport_series: sd.passport_series || '', passport_number: sd.passport_number || '' }); } results.push({ student_key: key, subject_code: currentSubject.code, subject_name: currentSubject.name, exam_date: currentSubject.date || '', primary_score: sd.primary_score, grade: sd.grade }); } } } return { subjects: uniqSubjects(parsedSubjects), students: [...studentsMap.values()], results }; } function uniqSubjects(subjects) { const seen = new Set(); return subjects.filter(s => { if (seen.has(s.code)) return false; seen.add(s.code); return true; }); } function detectSubject(firstCell) { if (!firstCell) return null; const match = firstCell.match(/^(\d{2})\s*-\s*(.+?)\s+(\d{4}\.\d{2}\.\d{2})$/); if (!match) return null; const code = match[1]; const name = match[2]; const date = match[3]; if (SUBJECT_NAMES.some(sn => sn.toLowerCase() === name.toLowerCase())) { return { code, name, date }; } return null; } function isSummaryRowText(firstCell) { return /^(средни|всего|итого)/i.test(firstCell); } function findColumnHeaders(row) { const map = {}; for (let i = 0; i < row.length; i++) { const s = normalize(String(row[i])); for (const [pattern, field] of Object.entries(HEADER_MAP)) { if (s === pattern || s.includes(pattern)) { map[field] = i; break; } } } if (map.last_name && (map.primary_score || map.grade)) return map; return null; } function extractStudentData(row, colIdx) { const get = (field) => { const idx = colIdx[field]; if (idx === undefined || idx >= row.length) return ''; return String(row[idx]).trim(); }; const firstCol = String(row[0] || '').trim(); if (!firstCol || !/^\d+$/.test(firstCol)) return null; const last_name = get('last_name'); if (!last_name || last_name.length < 2) return null; const toInt = (v) => { const n = parseInt(v, 10); return isNaN(n) ? null : n; }; return { last_name, first_name: get('first_name'), patronymic: get('patronymic'), class: get('class'), school_code: get('school_code'), passport_series: get('passport_series'), passport_number: get('passport_number'), primary_score: toInt(get('primary_score')), grade: toInt(get('grade')) }; } module.exports = { parseXls };