Files

158 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import re
from datetime import datetime
from typing import Iterable
import pandas as pd
from .base import norm
# Shared date support for all bank parsers. Indian bank statements and the
# other supported banks can be rendered differently by pdftotext/pdfplumber,
# so parsers must not depend on one literal date layout.
SUPPORTED_DATE_FORMATS: tuple[str, ...] = (
"%d %b %Y",
"%d %B %Y",
"%b %d %Y",
"%B %d %Y",
"%d %b, %Y",
"%d %B, %Y",
"%b %d, %Y",
"%B %d, %Y",
"%d-%b-%Y",
"%d-%B-%Y",
"%d/%b/%Y",
"%d/%B/%Y",
"%d-%m-%Y",
"%d/%m/%Y",
"%d.%m.%Y",
"%Y-%m-%d",
"%Y/%m/%d",
"%d %b %y",
"%d %B %y",
"%b %d %y",
"%B %d %y",
"%d-%b-%y",
"%d-%B-%y",
"%d/%b/%y",
"%d/%B/%y",
"%d-%m-%y",
"%d/%m/%y",
"%d.%m.%y",
)
# A permissive token used only to locate a candidate date at the start of a
# transaction line. parse_flexible_date() performs the actual validation.
DATE_TOKEN_PATTERN = (
r"(?:"
r"\d{1,2}\s+[A-Za-z]{3,9},?\s+\d{2,4}"
r"|[A-Za-z]{3,9}\s+\d{1,2},?\s+\d{2,4}"
r"|\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4}"
r"|\d{4}[-/.]\d{1,2}[-/.]\d{1,2}"
r"|\d{1,2}[-/.][A-Za-z]{3,9}[-/.]\d{2,4}"
r")"
)
DATE_TOKEN_RE = re.compile(DATE_TOKEN_PATTERN, re.I)
LEADING_DATE_RE = re.compile(rf"^\s*({DATE_TOKEN_PATTERN})(?:\s+|$)(.*)$", re.I)
def find(pattern, text, group=1, flags=re.I | re.M):
m = re.search(pattern, text, flags)
return norm(m.group(group)) if m else ""
def _normalise_date_text(value: str) -> str:
value = norm(value)
value = value.replace("", "-").replace("", "-")
value = re.sub(r"\s*,\s*", ", ", value)
value = re.sub(r"\s+", " ", value)
return value.strip(" ,")
def parse_flexible_date(value, *, default_year: int | None = None) -> pd.Timestamp | pd.NaT:
"""Parse an Indian bank date without silently swapping day and month.
Explicit formats are attempted before pandas' parser. Numeric dates are
always interpreted day-first because all supported statements are Indian
banking statements. A default year may be supplied for legacy DD/MM rows.
"""
if value is None or (isinstance(value, float) and pd.isna(value)):
return pd.NaT
if isinstance(value, (pd.Timestamp, datetime)):
return pd.Timestamp(value)
text = _normalise_date_text(str(value))
if not text:
return pd.NaT
if default_year and re.fullmatch(r"\d{1,2}[-/.]\d{1,2}", text):
text = f"{text}/{default_year}"
for fmt in SUPPORTED_DATE_FORMATS:
try:
return pd.Timestamp(datetime.strptime(text, fmt))
except ValueError:
continue
# Final guarded fallback for extraction noise. dayfirst=True is explicit
# and yearfirst is enabled only when the candidate begins with four digits.
parsed = pd.to_datetime(
text,
errors="coerce",
dayfirst=not bool(re.match(r"^\d{4}[-/.]", text)),
yearfirst=bool(re.match(r"^\d{4}[-/.]", text)),
)
return pd.NaT if pd.isna(parsed) else pd.Timestamp(parsed)
def date_iso(value, *, default_year: int | None = None) -> str:
parsed = parse_flexible_date(value, default_year=default_year)
return "" if pd.isna(parsed) else parsed.strftime("%Y-%m-%d")
def leading_dates(line: str, *, maximum: int = 2) -> tuple[list[str], str]:
"""Return up to ``maximum`` validated dates from the start of a line."""
remaining = line
dates: list[str] = []
for _ in range(maximum):
match = LEADING_DATE_RE.match(remaining)
if not match:
break
candidate = match.group(1)
if pd.isna(parse_flexible_date(candidate)):
break
dates.append(candidate)
remaining = match.group(2)
return dates, remaining
def find_date_tokens(text: str) -> list[str]:
return [m.group(0) for m in DATE_TOKEN_RE.finditer(text or "") if not pd.isna(parse_flexible_date(m.group(0)))]
def split_pages(text):
return text.split("\f")
def infer_mode(n):
u = (n or "").upper()
for k, v in (
("UPI", "UPI"),
("NEFT", "NEFT"),
("IMPS", "IMPS"),
("RTGS", "RTGS"),
("CASH DEPOSIT", "Cash Deposit"),
("CASH WITHDRAWAL", "Cash Withdrawal"),
("ATM", "ATM"),
("CHEQUE", "Cheque"),
("CHQ", "Cheque"),
("POS", "POS"),
("EDC", "Card Settlement"),
("ACH", "ACH"),
):
if k in u:
return v
return "Other"