| """Robust parsers for the messy strings you find in receipt / invoice ground truth. |
| |
| Public dataset ground truth is dirty: money values arrive as "$12.99", "12,99", |
| "12.99 USD", or even "1299" (missing decimal). Dates come in "DD/MM/YYYY", |
| "MM-DD-YY", "20250615", "Jun 15, 2025", etc. |
| |
| These parsers do best-effort normalization and return None on failure — the |
| caller decides whether that's an error. |
| """ |
| from __future__ import annotations |
|
|
| import re |
| from datetime import date, datetime |
|
|
| |
|
|
| |
| |
| _MONEY_STRIP_RE = re.compile(r"[^\d.,\-]") |
|
|
|
|
| def parse_money(value: str | float | int | None) -> float | None: |
| """Parse a monetary string/number to a float. Returns None on failure. |
| |
| Handles: |
| "$12.99" -> 12.99 |
| "12,99" -> 12.99 (European decimal) |
| "1,299.50" -> 1299.50 |
| "1.299,50" -> 1299.50 (EU format with thousands) |
| 1299 -> 1299.0 |
| """ |
| if value is None: |
| return None |
| if isinstance(value, (int, float)): |
| return round(float(value), 2) |
|
|
| s = _MONEY_STRIP_RE.sub("", str(value)).strip() |
| if not s: |
| return None |
|
|
| |
| last_comma = s.rfind(",") |
| last_dot = s.rfind(".") |
|
|
| try: |
| if last_comma > last_dot: |
| |
| s = s.replace(".", "").replace(",", ".") |
| else: |
| |
| s = s.replace(",", "") |
| return round(float(s), 2) |
| except ValueError: |
| return None |
|
|
|
|
| |
|
|
| |
| |
| _DATE_FORMATS = ( |
| "%Y-%m-%d", |
| "%Y/%m/%d", |
| "%d-%m-%Y", |
| "%d/%m/%Y", |
| "%m-%d-%Y", |
| "%m/%d/%Y", |
| "%d-%m-%y", |
| "%d/%m/%y", |
| "%b %d, %Y", |
| "%B %d, %Y", |
| "%d %b %Y", |
| "%d %B %Y", |
| "%Y%m%d", |
| ) |
|
|
|
|
| def parse_date(value: str | date | None) -> date | None: |
| """Parse a date string using a family of common formats. None on failure.""" |
| if value is None: |
| return None |
| if isinstance(value, date): |
| return value |
|
|
| s = str(value).strip() |
| if not s: |
| return None |
|
|
| for fmt in _DATE_FORMATS: |
| try: |
| return datetime.strptime(s, fmt).date() |
| except ValueError: |
| continue |
| return None |
|
|
|
|
| |
|
|
|
|
| def clean_text(value: str | None) -> str | None: |
| """Collapse whitespace, strip. Returns None if the result is empty.""" |
| if not value: |
| return None |
| cleaned = " ".join(str(value).split()) |
| return cleaned or None |
|
|