176 lines
5.6 KiB
Python
176 lines
5.6 KiB
Python
"""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", "<na>"):
|
|
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)"
|
|
)
|
|
|