"""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, )