"""Mapowanie układu kolumn na schemat kanoniczny. Setki plików mogą mieć te same dane pod różnymi nagłówkami i w różnej kolejności kolumn ("Imię", "Name", "NAZWA" -> kanoniczne 'name'). Ta warstwa tłumaczy faktyczny układ kolumn pliku na wspólny słownik pól, dzięki czemu reszta systemu (i przyszła baza SQL) operuje na jednej, stabilnej nazwie pola. Aliasowanie jest świadomie wydzielone i konfigurowalne — to jedyne miejsce do edycji, gdy pojawi się nowy wariant nagłówka. """ from __future__ import annotations import re # kanoniczne_pole -> zbiór aliasów (po normalizacji) CANONICAL_ALIASES: dict[str, set[str]] = { "id": {"id", "identyfikator", "nr", "no", "number"}, "name": {"name", "imie", "nazwa", "nazwisko", "title", "tytul"}, "symbol": {"symbol", "znak", "sign", "glyph"}, "date": {"date", "data", "datetime", "timestamp"}, "value": {"value", "wartosc", "val", "amount", "kwota"}, "category": {"category", "kategoria", "type", "typ", "group", "grupa"}, } def _normalize(col: str) -> str: s = str(col).strip().lower() s = re.sub(r"[ąàá]", "a", s) s = s.replace("ł", "l").replace("ż", "z").replace("ź", "z").replace("ć", "c") s = s.replace("ę", "e").replace("ó", "o").replace("ś", "s").replace("ń", "n") s = re.sub(r"[^a-z0-9]+", "", s) return s def build_column_mapping(header_cells: list[str]) -> dict[str, str]: """Zwraca mapę pole_kanoniczne -> faktyczna_nazwa_kolumny dla danego pliku. Kolumny nierozpoznane są zachowywane pod swoją (znormalizowaną) nazwą, więc nic nie ginie — po prostu nie mają aliasu kanonicznego. """ reverse: dict[str, str] = {} for canonical, aliases in CANONICAL_ALIASES.items(): for alias in aliases: reverse[alias] = canonical mapping: dict[str, str] = {} for actual in header_cells: norm = _normalize(actual) canonical = reverse.get(norm, norm or "col") # pierwsze trafienie wygrywa (stabilność przy duplikatach) mapping.setdefault(canonical, actual) return mapping