feat: Enhance CSV cleaning service with new endpoints for CSV analysis and preview functionality. Introduce support for job management linked to extraction batches, and update the cleaning rules structure. Add new utility functions for handling CSV data and improve documentation for clarity on usage and available endpoints.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
"""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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user