"""Homologación y división de tipo/número de documento.""" from __future__ import annotations import re import unicodedata from typing import Any, Optional import pandas as pd from .nullish import _is_nullish_scalar _DOC_TYPE_ALIASES: dict[str, tuple[str, ...]] = { "CC": ("CC", "Cedula", "CD", "C", "C.C", "C C", "CCC"), "TI": ("TI", "Tarjeta", "Tarjeta Identidad", "T1", "T", "TL"), "CN": ("CN", "Certificado", "Nacido Vivo", "Certificado Nacido Vivo"), "CE": ("CE", "Cedula de Extranjeria", "Extranjera"), "ME": ("ME", "Menor sin Identificacion"), "PS": ("PS", "Pasaporte"), "PT": ("PT", "Permiso por proteccion Temporal"), "RC": ("RC", "Registro Civil", "Registro", "TC"), "SC": ("SC", "Salvo Conducto", "Salvoconducto"), "PE": ("PE", "Permiso especial de permanencia", "Permiso"), } def _ascii_upper(value: Any) -> str: text = unicodedata.normalize("NFKD", str(value or "")) text = "".join(c for c in text if not unicodedata.combining(c)) return text.upper() def _normalize_doc_type_key(value: Any) -> str: """Clave comparable: sin acentos, espacios, puntos ni símbolos.""" return re.sub(r"[^A-Z0-9]", "", _ascii_upper(value)) def _clean_doc_type_text(value: Any) -> Optional[str]: cleaned = re.sub(r"[^A-Z0-9]+", " ", _ascii_upper(value)).strip() cleaned = re.sub(r"\s+", " ", cleaned) return cleaned or None _DOC_TYPE_ALIAS_BY_KEY: dict[str, str] = { _normalize_doc_type_key(alias): canonical for canonical, aliases in _DOC_TYPE_ALIASES.items() for alias in aliases } _DOC_TYPE_ALIAS_DISPLAY_BY_KEY: dict[str, str] = {} for _canonical, _aliases in _DOC_TYPE_ALIASES.items(): for _alias in _aliases: _alias_key = _normalize_doc_type_key(_alias) _alias_display = _clean_doc_type_text(_alias) if _alias_key and _alias_display: _DOC_TYPE_ALIAS_DISPLAY_BY_KEY.setdefault(_alias_key, _alias_display) _DOC_TYPE_ALIAS_KEYS_SORTED: list[str] = sorted( _DOC_TYPE_ALIAS_BY_KEY, key=lambda k: (len(k), k), reverse=True, ) def homologate_doc_type(value: Any) -> Optional[str]: """Homologa un tipo documental a sigla; si no existe, conserva texto limpio.""" if _is_nullish_scalar(value): return None key = _normalize_doc_type_key(value) if not key or key.lower() in ("nan", "none", "nat"): return None return _DOC_TYPE_ALIAS_BY_KEY.get(key) or _clean_doc_type_text(value) def _compact_doc_text(value: Any) -> str: return _normalize_doc_type_key(value) def _looks_like_short_alias_prefix(compact: str, alias_key: str) -> bool: """Evita que alias de 1-2 chars capturen palabras como CODIGO o TOTAL.""" if not compact.startswith(alias_key): return False rest = compact[len(alias_key):] if not rest: return True return bool(rest[0].isdigit()) def _match_known_doc_type_prefix(compact: str) -> Optional[tuple[str, str]]: for alias_key in _DOC_TYPE_ALIAS_KEYS_SORTED: if len(alias_key) <= 2: if not _looks_like_short_alias_prefix(compact, alias_key): continue elif not compact.startswith(alias_key): continue return _DOC_TYPE_ALIAS_BY_KEY[alias_key], alias_key return None def split_doc_tipo_y_numero_texto( raw: Any, *, homologate: bool = True, ) -> tuple[Optional[str], Optional[str]]: """Separa tipo documental y número, tolerando nombres largos y símbolos. Si `homologate` es True, tipos conocidos se devuelven como sigla (CC, TI, etc.). Los tipos desconocidos se conservan limpios en mayúsculas. """ if _is_nullish_scalar(raw) and not (isinstance(raw, str) and not str(raw).strip()): return (None, None) t = str(raw).strip() if t.lower() in ("nan", "none", "nat", ""): return (None, None) if t and t[0].isdigit(): return (None, None) compact = _compact_doc_text(t) if not compact: return (None, None) match = _match_known_doc_type_prefix(compact) if match is not None: canonical, alias_key = match rest = compact[len(alias_key):] nums = re.sub(r"[^\d]", "", rest) if homologate: return (canonical, nums if nums else None) raw_type = _DOC_TYPE_ALIAS_DISPLAY_BY_KEY.get(alias_key) or canonical return (raw_type, nums if nums else None) m = re.match( r"^([^\d]+?)\s*([0-9].*)\s*$", t, ) if m: to_t = _clean_doc_type_text(m.group(1)) nums = re.sub(r"[^\d]", "", m.group(2)) return (homologate_doc_type(to_t) if homologate else to_t, nums if nums else None) if not re.search(r"\d", t): to_t = homologate_doc_type(t) if homologate else _clean_doc_type_text(t) return (to_t, None) return (None, None) def _alloc_doc_split_column_names( col_name: str, existing: set[str] ) -> tuple[str, str]: """Nombres únicos para las dos columnas derivadas.""" base = re.sub(r"[^\w]+", "_", str(col_name).strip(), flags=re.ASCII) or "col" for i in range(0, 200): suf = f"_{i}" if i else "" t, n = f"{base}{suf}_doc_tipo", f"{base}{suf}_doc_numeros" if t not in existing and n not in existing: return t, n raise ValueError("No se pudieron asignar nombres de columnas únicos.") def _apply_doc_id_split_columns( result: pd.DataFrame, col: str, log: list[str], *, homologate_type: bool = True ) -> None: tcol, ncol = _alloc_doc_split_column_names(str(col), set(result.columns)) st_tipo: list[Optional[str]] = [] st_num: list[Any] = [] for v in result[col].tolist(): tt, nn = split_doc_tipo_y_numero_texto(v, homologate=homologate_type) st_tipo.append(tt) st_num.append(nn if nn is not None else pd.NA) result[tcol] = st_tipo result[ncol] = st_num log.append( f"[OK] '{str(col)}' -> columnas '{tcol}' y '{ncol}' <- Dividir tipo y número (documento)" )