69 lines
4.0 KiB
TypeScript
69 lines
4.0 KiB
TypeScript
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: `<tr class="evaluated">")
|
|
// 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<string, number>();
|
|
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 };
|
|
}
|