import type { SheetAnalysis } from '../domain/dataset'; import { clean } from '../shared/text'; import { profileColumn } from './column-profiler'; import { detectHeaderRow, isKeyLabel, isKeyValueTable, uniqueHeaders } from './header-detector'; import { buildRecord } from './record-builder'; export function analyzeRows(rows: string[][], sheetName: string, sourceFile = '', highlightedRows?: boolean[]): SheetAnalysis { // Die Markierung eines Berichts (z. B. CIS-CAT: `") // haengt an der Zeile, nicht an ihrem Index - Kopfzeilenerkennung und das // Herausfiltern leerer Zeilen verschieben Indizes. Zeile und Markierung // laufen deshalb ab hier als Paar durch dieselben Filter/Slice-Schritte. const withFlags = rows.map((row, index) => ({ row, highlighted: highlightedRows?.[index] ?? false })); const compactEntries = withFlags.filter(entry => entry.row.some(cell => clean(cell))); const compactRows = compactEntries.map(entry => entry.row); if (!compactRows.length) { return { headerIndex: 0, headers: [], records: [], columns: [], signals: ['Keine auswertbaren Inhalte gefunden.'], title: '' }; } // Eine einzelne Zeile hat nichts, wovon sie eine Kopfzeile sein koennte - // die Kopfzeilenerkennung waehlt trotzdem immer einen Index (und sei es der // einzige), und die Zeile verschwindet spurlos als vermeintlicher Kopf ohne // jede Datenzeile darunter. CIS-CAT gibt so z. B. den Hinweis // "No matching system items were found." als einzeilige Belegtabelle aus - // ohne diese Ausnahme stand dort ein leeres Tabellengeruest ohne Inhalt. const singleRow = compactRows.length === 1; // Eine Schluessel/Wert-Tabelle hat keine Kopfzeile: jede Zeile ist ein // Wertepaar, und eine vorangestellte Zeile ohne Wert ist ihr Titel. Die // Spaltennamen sind deshalb hier - und nur hier - vergeben statt gelesen. const keyValue = !singleRow && isKeyValueTable(compactRows); const headerIndex = singleRow ? -1 : keyValue ? Math.max(0, compactRows.findIndex(row => isKeyLabel(row[0]))) : detectHeaderRow(compactRows); const title = headerIndex > 0 ? compactRows.slice(0, headerIndex).flat().map(clean).filter(Boolean).join(' · ') : ''; const headers = singleRow ? (compactRows[0].length === 1 ? ['Hinweis'] : uniqueHeaders(compactRows[0].map(() => ''))) : keyValue ? ['Feld', 'Wert'] : uniqueHeaders(compactRows[headerIndex] ?? []); const dataEntries = singleRow ? compactEntries : compactEntries .slice(keyValue ? headerIndex : headerIndex + 1) .filter(entry => entry.row.some(cell => clean(cell))); const dataRows = dataEntries.map(entry => entry.row); const occurrences = new Map(); const records = dataEntries.map((entry, index) => { const signature = JSON.stringify(entry.row.map(clean)); const occurrence = occurrences.get(signature) ?? 0; occurrences.set(signature, occurrence + 1); return buildRecord({ row: entry.row, headers, index, sourceFile, sheetName, occurrence, highlighted: entry.highlighted }); }); const columns = headers.map((header, columnIndex) => profileColumn(header, dataRows.map(row => row[columnIndex]))); const signals: string[] = []; if (title) signals.push(`Titelbereich erkannt: ${title.slice(0, 120)}`); if (keyValue) signals.push('Schlüssel/Wert-Tabelle ohne Kopfzeile erkannt: jede Zeile ist ein Wertepaar.'); else if (headerIndex > 0) signals.push(`Die eigentliche Kopfzeile wurde in Zeile ${headerIndex + 1} erkannt.`); const sparse = columns.filter(column => column.total && column.fillRate < 20).map(column => column.header).slice(0, 4); if (sparse.length) signals.push(`Sehr leere Felder: ${sparse.join(', ')}.`); if (!records.length) signals.push('Nur Struktur erkannt, aber keine Datensätze unterhalb der Kopfzeile.'); if (!signals.length) signals.push('Struktur wirkt konsistent: Kopfzeile, Datensätze und Felder konnten abgeleitet werden.'); return { headerIndex, headers, records, columns, signals, title }; }