171 lines
4.7 KiB
JavaScript
171 lines
4.7 KiB
JavaScript
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 };
|