const XLSX = require("xlsx"); const fs = require("fs"); // Чтение файлов const wb1 = XLSX.readFile("БП_2026_НСВМ.xlsx"); const wb2 = XLSX.readFile("Шаблон БП_ БКВ 2027 с займами.xlsx"); // Получение списка всех листов const sheets1 = wb1.SheetNames; const sheets2 = wb2.SheetNames; console.log("Файл 1 листы:", sheets1); console.log("Файл 2 листы:", sheets2); // Сравнительные данные const comparisonResults = []; // Сравниваем листы const allSheets = [...new Set([...sheets1, ...sheets2])]; allSheets.forEach(sheetName => { const sheet1Exists = sheets1.includes(sheetName); const sheet2Exists = sheets2.includes(sheetName); if (!sheet1Exists) { comparisonResults.push({ "Лист": sheetName, "Статус": "Отсутствует в файле 1 (БП_2026_НСВМ)", "Различия": "Лист есть только во втором файле" }); return; } if (!sheet2Exists) { comparisonResults.push({ "Лист": sheetName, "Статус": "Отсутствует в файле 2 (Шаблон БП_ БКВ 2027 с займами)", "Различия": "Лист есть только в первом файле" }); return; } // Получаем данные листов const data1 = XLSX.utils.sheet_to_json(wb1.Sheets[sheetName], { header: 1 }); const data2 = XLSX.utils.sheet_to_json(wb2.Sheets[sheetName], { header: 1 }); // Сравниваем размеры const rows1 = data1.length; const rows2 = data2.length; const cols1 = data1.length > 0 ? Math.max(...data1.map(r => r.length)) : 0; const cols2 = data2.length > 0 ? Math.max(...data2.map(r => r.length)) : 0; if (rows1 !== rows2 || cols1 !== cols2) { comparisonResults.push({ "Лист": sheetName, "Статус": "Разные размеры", "Различия": `Файл 1: ${rows1} строк x ${cols1} колонок | Файл 2: ${rows2} строк x ${cols2} колонок` }); } // Построчное сравнение (первые 100 строк для производительности) const maxRows = Math.min(Math.max(rows1, rows2), 100); let diffCount = 0; const diffDetails = []; for (let i = 0; i < maxRows; i++) { const row1 = data1[i] || []; const row2 = data2[i] || []; const maxCols = Math.max(row1.length, row2.length); for (let j = 0; j < maxCols; j++) { const val1 = row1[j] !== undefined ? row1[j] : null; const val2 = row2[j] !== undefined ? row2[j] : null; if (val1 !== val2 && val1 !== null && val2 !== null) { diffCount++; if (diffDetails.length < 20) { // Сохраняем первые 20 различий const colLabel = String.fromCharCode(65 + j); // A, B, C... const rowNum = i + 1; diffDetails.push(`Ячейка ${colLabel}${rowNum}: "${val1}" ≠ "${val2}"`); } } } } if (diffCount > 0) { comparisonResults.push({ "Лист": sheetName, "Статус": "Есть различия", "Различия": `Найдено различий: ${diffCount}. Примеры: ${diffDetails.slice(0, 5).join("; ")}` }); } else { comparisonResults.push({ "Лист": sheetName, "Статус": "Идентичен", "Различия": "Нет" }); } }); // Создаём итоговый файл с результатами сравнения const resultWorkbook = XLSX.utils.book_new(); // Лист 1: Сводная таблица сравнения const summaryData = [ ["СРАВНЕНИЕ ФАЙЛОВ"], ["Файл 1:", "БП_2026_НСВМ.xlsx"], ["Файл 2:", "Шаблон БП_ БКВ 2027 с займами.xlsx"], ["Дата сравнения:", new Date().toLocaleString("ru-RU")], [], ["СВОДНАЯ ТАБЛИЦА РАЗЛИЧИЙ"], ["Лист", "Статус", "Различия"] ]; comparisonResults.forEach(r => { summaryData.push([r["Лист"], r["Статус"], r["Различия"]]); }); const summarySheet = XLSX.utils.json_to_sheet(summaryData.slice(7), { skipHeader: false }); XLSX.utils.sheet_add_aoa(summarySheet, summaryData.slice(0, 7), { origin: "A1" }); XLSX.utils.book_append_sheet(resultWorkbook, summarySheet, "Сводка"); // Лист 2: Детальное сравнение по ячейкам const detailData = [["Лист", "Ячейка", "Значение в файле 1", "Значение в файле 2", "Тип различия"]]; sheets1.forEach(sheetName => { if (!sheets2.includes(sheetName)) return; const data1 = XLSX.utils.sheet_to_json(wb1.Sheets[sheetName], { header: 1 }); const data2 = XLSX.utils.sheet_to_json(wb2.Sheets[sheetName], { header: 1 }); const maxRows = Math.min(Math.max(data1.length, data2.length), 200); for (let i = 0; i < maxRows; i++) { const row1 = data1[i] || []; const row2 = data2[i] || []; const maxCols = Math.max(row1.length, row2.length); for (let j = 0; j < maxCols; j++) { const val1 = row1[j] !== undefined ? row1[j] : null; const val2 = row2[j] !== undefined ? row2[j] : null; if (val1 !== val2 && val1 !== null && val2 !== null) { const colLabel = String.fromCharCode(65 + j); const rowNum = i + 1; detailData.push([ sheetName, `${colLabel}${rowNum}`, String(val1), String(val2), typeof val1 !== typeof val2 ? "Разный тип данных" : "Разное значение" ]); } } } }); const detailSheet = XLSX.utils.json_to_sheet(detailData.slice(1), { skipHeader: false }); XLSX.utils.sheet_add_aoa(detailSheet, [detailData[0]], { origin: "A1" }); XLSX.utils.book_append_sheet(resultWorkbook, detailSheet, "Детали различий"); // Лист 3: Структура файлов const structureData = [ ["СТРУКТУРА ФАЙЛОВ"], [], ["Файл 1: БП_2026_НСВМ.xlsx"], ["№", "Название листа", "Строк", "Колонок"] ]; sheets1.forEach((name, idx) => { const sheet = XLSX.utils.sheet_to_json(wb1.Sheets[name], { header: 1 }); const rows = sheet.length; const cols = rows > 0 ? Math.max(...sheet.map(r => r.length)) : 0; structureData.push([idx + 1, name, rows, cols]); }); structureData.push([], [], ["Файл 2: Шаблон БП_ БКВ 2027 с займами.xlsx"], ["№", "Название листа", "Строк", "Колонок"]); sheets2.forEach((name, idx) => { const sheet = XLSX.utils.sheet_to_json(wb2.Sheets[name], { header: 1 }); const rows = sheet.length; const cols = rows > 0 ? Math.max(...sheet.map(r => r.length)) : 0; structureData.push([idx + 1, name, rows, cols]); }); const structureSheet = XLSX.utils.json_to_sheet(structureData.slice(4), { skipHeader: false }); XLSX.utils.sheet_add_aoa(structureSheet, structureData.slice(0, 4), { origin: "A1" }); XLSX.utils.book_append_sheet(resultWorkbook, structureSheet, "Структура файлов"); // Сохраняем результат const outputFile = "Сравнение_БП_2026_НСВМ_и_Шаблон_БП_БКВ_2027.xlsx"; XLSX.writeFile(resultWorkbook, outputFile); console.log(`\n✅ Сравнение завершено!`); console.log(`📊 Файл с результатами: ${outputFile}`); console.log(`📈 Всего листов сравнено: ${allSheets.length}`); console.log(`⚠️ Листов с различиями: ${comparisonResults.filter(r => r["Статус"] === "Есть различия").length}`); console.log(`📋 Листов без различий: ${comparisonResults.filter(r => r["Статус"] === "Идентичен").length}`);