Files
udpak_semistruktur/udpak_semistruktur/transform/convert.py
2026-04-02 23:36:17 +02:00

196 lines
7.8 KiB
Python

import re
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP
from typing import Any, Optional
from udpak_semistruktur.logger import hent_logger
logger = hent_logger(__name__)
def _parse_number(value: Any) -> Decimal:
"""
Konverterer en talværdi til Decimal.
Håndterer EU-format (punktum som tusindtalsseparator, komma som decimal)
samt US-format og rene integers. Kaster ValueError ved ugyldigt format.
"""
if isinstance(value, (int, float, Decimal)):
return Decimal(str(value))
s = str(value).strip()
# Fjern valuta og ikke-tal (men bevar cifre, komma, punktum og minus)
s = re.sub(r'[^\d,.\-]', '', s)
# Håndter typiske EU-formater
if ',' in s and '.' in s:
# Antag '.' = tusind, ',' = decimal, fx "391.211,75" -> "391211.75"
s = s.replace('.', '').replace(',', '.')
elif ',' in s:
# Kun komma => decimal komma, fx "391211,75" -> "391211.75"
s = s.replace(',', '.')
else:
# Kun punktum eller rene cifre -> int/US-format
pass
# Tom streng eller bare "-" er ugyldig
if s in ('', '-', '.'):
raise ValueError(f"Ugyldigt talformat: {value!r}")
return Decimal(s)
def konverter(data: dict, file_config: dict, global_config: dict) -> dict:
"""
Konverterer kolonneværdier i data til de typer der er angivet i file_config.
Håndterer string, integer, float, decimal, boolean og date.
Rækker med fejl samles i data['fejlede_rækker'] hvis fejl_fil er konfigureret,
ellers kastes en exception.
"""
if not isinstance(data, dict) or "rækker" not in data:
return data
fejl_fil = global_config.get("fejl_fil_ext", None)
kolonner = file_config.get("kolonner", [])
nye_rækker = []
fejlede_rækker = []
for række in data["rækker"]:
ny_række = række.copy()
fejl_i_række = False
for kolonne in kolonner:
field_type = kolonne.get("type", "string")
string_max_len = kolonne.get("max_længde", None)
string_truncate = kolonne.get("truncate", None)
if field_type == "string" and not string_max_len and not string_truncate:
continue
kolonnenavn = kolonne.get("navn")
value = ny_række.get(kolonnenavn)
krav = kolonne.get("påkrævet", False)
# Hvis værdien er None
if value is None:
if krav:
logger.warning(f"Påkrævet felt '{kolonnenavn}' mangler.")
if fejl_fil:
fejl_i_række = True
break
else:
raise ValueError(f"Påkrævet felt '{kolonnenavn}' mangler.")
else:
continue # spring konvertering over for denne kolonne
try:
if field_type in ["integer", "biginteger", "bigint"]:
tmp = int(value)
elif field_type in ["float", "decimal"]:
dec = _parse_number(value)
decimal_places = kolonne.get("decimaler", 2)
q = Decimal(10) ** (-decimal_places) # fx 2 -> Decimal('0.01')
dec = dec.quantize(q, rounding=ROUND_HALF_UP)
if field_type == "float":
tmp = f"{dec:.{decimal_places}f}"
else:
# "decimal" som streng bevaret med præcision
tmp = f"{dec:.{decimal_places}f}"
elif field_type == "boolean":
tmp = str(value).lower() in ["true", "1", "ja"]
elif field_type == "date":
tmp_value = str(value) # altid str
if '[' in tmp_value and tmp_value.endswith(']'):
tmp_value = tmp_value[:tmp_value.index('[')]
dato_ind_raw = kolonne.get("dato_ind", global_config.get("dato_ind"))
dato_ud = kolonne.get("dato_ud", global_config.get("dato_ud"))
# Tillad både string og liste
if isinstance(dato_ind_raw, str):
dato_ind_liste = [dato_ind_raw]
elif isinstance(dato_ind_raw, list):
dato_ind_liste = dato_ind_raw
else:
raise ValueError(f"'dato_ind' skal være streng eller liste, men var: {type(dato_ind_raw)}")
tmp_dato = None
parse_errors = []
for dato_ind in dato_ind_liste:
try:
if "%f" in dato_ind:
if re.search(r'\.\d+', tmp_value):
tmp_value_padded = re.sub(
r'\.(\d{1,6})',
lambda m: '.' + m.group(1).ljust(6, '0'),
tmp_value
)
else:
dato_ind = dato_ind.replace(".%f", "")
tmp_value_padded = tmp_value
else:
tmp_value_padded = tmp_value
# Fjern kolon i tidszonedelen: +03:00 → +0300, hvis %z bruges
if "%z" in dato_ind:
tmp_value_padded = re.sub(r'([+-]\d{2}):(\d{2})$', r'\1\2', tmp_value_padded)
tmp_dato = datetime.strptime(tmp_value_padded, dato_ind)
break # succes!
except ValueError as e:
parse_errors.append(f" - Format: {dato_ind} -> {e}")
if tmp_dato is None:
fejlbesked = "\n".join(parse_errors)
raise ValueError(f"Kunne ikke parse dato '{tmp_value}' med nogen af formaterne:\n{fejlbesked}")
# Output-format
if dato_ud.upper().strip() == "SYBASE":
tmp = tmp_dato.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
elif dato_ud.upper().strip() == "DATE":
tmp = tmp_dato.strftime('%Y-%m-%d')
elif dato_ud.upper().strip() == "INFORMATICA_US":
tmp = tmp_dato.strftime('%m/%d/%Y %H:%M:%S.%f')
else:
tmp = tmp_dato.strftime(dato_ud)
elif field_type == "string":
value = str(value)
if string_max_len and len(value) > string_max_len:
tmp = value[:string_max_len - 3] + "..."
elif string_truncate and len(value) > string_truncate:
tmp = value[:string_truncate]
else:
tmp = value
elif field_type in ["hash", "id", "file", "rod_variant"]:
tmp = value
else:
raise ValueError(f"Ukendt datatype '{field_type}' for feltet '{kolonnenavn}'.")
ny_række[kolonnenavn] = tmp
except (ValueError, TypeError) as e:
logger.error(f"[CONVERT]Fejl ved konvertering af felt '{kolonnenavn}' med værdi '{value}': {e}")
if fejl_fil:
fejl_i_række = True
break # Stop konvertering af denne række
else:
raise e
if fejl_i_række:
fejlede_rækker.append(række) # Tilføj original række
else:
nye_rækker.append(ny_række)
# Overskriv med gyldige rækker
data["rækker"] = nye_rækker
if fejl_fil:
data["fejlede_rækker"] = fejlede_rækker
return data