142 lines
4.3 KiB
Python
142 lines
4.3 KiB
Python
"""Reglas de normalización de fechas a DD/MM/AAAA."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
import pandas as pd
|
|
|
|
from ..dates import (
|
|
_detect_compact_date_formats,
|
|
_normalize_messy_date_to_compact,
|
|
_parse_any_date_to_tuple,
|
|
_parse_date_with_format,
|
|
_parse_date_with_text_month,
|
|
detect_date_format,
|
|
)
|
|
from ..nullish import _is_nullish_scalar
|
|
|
|
|
|
def _rule_date_to_ddmmyyyy(series: pd.Series) -> pd.Series:
|
|
"""Normaliza fechas numéricas, alfanuméricas, con meses en texto y con ruido."""
|
|
stripped = series.astype(str).str.replace(r"[\s\/\-\.]", "", regex=True)
|
|
stripped = stripped.where(series.notna(), other=pd.NA)
|
|
sep_fmt = detect_date_format(series) or detect_date_format(stripped)
|
|
compact8_fmt, compact6_fmt = _detect_compact_date_formats(series)
|
|
|
|
def _fn(x: Any):
|
|
if pd.isna(x) or str(x).strip() == "":
|
|
return x
|
|
parsed = _parse_any_date_to_tuple(
|
|
x,
|
|
sep_fmt=sep_fmt,
|
|
compact8_fmt=compact8_fmt,
|
|
compact6_fmt=compact6_fmt,
|
|
)
|
|
if parsed is None:
|
|
return x
|
|
d, m, y = parsed
|
|
return f"{d:02d}/{m:02d}/{y:04d}"
|
|
|
|
return series.map(_fn)
|
|
|
|
|
|
def _rule_date_numeric_to_ddmmyyyy(series: pd.Series) -> pd.Series:
|
|
"""Entrada: fechas en formato numérico (ej. 20240315 o 15032024).
|
|
|
|
Salida: "DD/MM/AAAA" tras detectar el formato predominante.
|
|
"""
|
|
# Para esta regla consideramos los valores "numéricos" (quitamos separadores
|
|
# comunes si los hay para detectar un YYYYMMDD/DDMMYYYY confiable).
|
|
stripped = series.astype(str).str.replace(r"[\s\/\-\.]", "", regex=True)
|
|
stripped = stripped.where(series.notna(), other=pd.NA)
|
|
fmt = detect_date_format(stripped)
|
|
if fmt is None:
|
|
# fallback: intentar con detector normal
|
|
fmt = detect_date_format(series)
|
|
return _format_dates_to_ddmmyyyy(series, fmt)
|
|
|
|
|
|
def _rule_date_alpha_to_ddmmyyyy(series: pd.Series) -> pd.Series:
|
|
"""Entrada: fechas en formato alfanumérico con separadores.
|
|
|
|
Salida: "DD/MM/AAAA".
|
|
"""
|
|
fmt = detect_date_format(series)
|
|
return _format_dates_to_ddmmyyyy(series, fmt)
|
|
|
|
|
|
def _format_dates_to_ddmmyyyy(series: pd.Series, fmt: Optional[str]) -> pd.Series:
|
|
"""Convierte cada valor al formato DD/MM/AAAA usando `fmt` como layout de
|
|
entrada detectado. La parte de hora (si existe) se descarta. Mantiene el
|
|
valor original si no puede parsear la fecha.
|
|
"""
|
|
if fmt is None:
|
|
return series.copy()
|
|
|
|
def _fn(x):
|
|
if pd.isna(x) or str(x).strip() == "":
|
|
return x
|
|
raw = str(x)
|
|
date_part, _time_part = _split_date_time(raw)
|
|
target = date_part if date_part else raw
|
|
parsed = _parse_date_with_format(target, fmt)
|
|
if parsed is None:
|
|
return x
|
|
d, m, y = parsed
|
|
return f"{d:02d}/{m:02d}/{y:04d}"
|
|
|
|
return series.map(_fn)
|
|
|
|
|
|
def _rule_date_messy_text_to_ddmmyyyy(series: pd.Series) -> pd.Series:
|
|
"""Fechas con símbolos y basura entre cifras → DD/MM/AAAA.
|
|
|
|
Normaliza a solo 6/8 dígitos, vota el formato con `detect_date_format` por
|
|
separado en compact8 y compact6; si el detector no devuelve nada, usa
|
|
`DDMMYYYY` / `DDMMYY` (alineado con la preferencia hispana en fechas
|
|
numéricas).
|
|
"""
|
|
cser = series.map(_normalize_messy_date_to_compact)
|
|
c8s = pd.Series(
|
|
[x for x in cser.tolist() if isinstance(x, str) and len(x) == 8]
|
|
)
|
|
c6s = pd.Series(
|
|
[x for x in cser.tolist() if isinstance(x, str) and len(x) == 6]
|
|
)
|
|
if c8s.empty and c6s.empty:
|
|
return series.copy()
|
|
|
|
fmt8: Optional[str] = None
|
|
fmt6: Optional[str] = None
|
|
if not c8s.empty:
|
|
fmt8 = detect_date_format(c8s) or "DDMMYYYY"
|
|
if not c6s.empty:
|
|
fmt6 = detect_date_format(c6s) or "DDMMYY"
|
|
|
|
def _one(x: Any, c: Any) -> Any:
|
|
if _is_nullish_scalar(x):
|
|
return x
|
|
if not isinstance(c, str) or len(c) not in (6, 8):
|
|
return x
|
|
f = (fmt8 or "DDMMYYYY") if len(c) == 8 else (fmt6 or "DDMMYY")
|
|
t = _parse_date_with_format(c, f)
|
|
if t is None and len(c) == 8:
|
|
t = _parse_date_with_format(c, "DDMMYYYY") or _parse_date_with_format(
|
|
c, "YYYYMMDD"
|
|
)
|
|
if t is None and len(c) == 6:
|
|
t = _parse_date_with_format(c, "DDMMYY")
|
|
if t is None:
|
|
return x
|
|
d, m, y = t
|
|
return f"{d:02d}/{m:02d}/{y:04d}"
|
|
|
|
return pd.Series(
|
|
[
|
|
_one(v, cser.iloc[i] if i < len(cser) else None) for i, v in enumerate(series)
|
|
],
|
|
index=series.index,
|
|
dtype=object,
|
|
)
|
|
|