from __future__ import annotations import re from decimal import Decimal, InvalidOperation, ROUND_HALF_UP from app.domain import ParsedTransaction, TransactionKind class ParseError(ValueError): pass _TRANSACTION_RE = re.compile( r"^\s*(?P[+-])?\s*" r"(?:(?Pдоход|приход|расход|трата)\s+)?" r"(?P\d[\d ]*(?:[,.]\d{1,2})?)\s*" r"(?:₽|р\.?|руб(?:лей|ля|ль)?\.?)?\s+" r"(?P\S.*)\s*$", flags=re.IGNORECASE, ) _DESCRIPTION_FIRST_RE = re.compile( r"^\s*(?P[+-])\s*(?Pзп|зарплата)\s+" r"(?P\d[\d ]*(?:[,.]\d{1,2})?)\s*" r"(?:₽|р\.?|руб(?:лей|ля|ль)?\.?)?\s*$", flags=re.IGNORECASE, ) _SALARY_RE = re.compile(r"(?:^|\s)(?:зп|зарплата)(?:$|\s)", flags=re.IGNORECASE) def parse_transaction(text: str) -> ParsedTransaction: match = _TRANSACTION_RE.match(text) description_first = False if not match: match = _DESCRIPTION_FIRST_RE.match(text) description_first = match is not None if not match: raise ParseError("Не удалось распознать сумму и описание") raw_amount = match.group("amount").replace(" ", "").replace(",", ".") try: amount = Decimal(raw_amount) except InvalidOperation as exc: raise ParseError("Некорректная сумма") from exc if amount <= 0: raise ParseError("Сумма должна быть больше нуля") amount_kopecks = int((amount * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)) description = " ".join(match.group("description").split()) kind_word = "" if description_first else (match.group("kind") or "").casefold() sign = match.group("sign") if sign == "-" and kind_word in {"доход", "приход"}: raise ParseError("Доход не может быть отрицательным") is_salary = _SALARY_RE.search(description) is not None kind = ( TransactionKind.INCOME if is_salary or kind_word in {"доход", "приход"} else TransactionKind.EXPENSE ) debt_effect = -1 if sign == "-" or kind is TransactionKind.INCOME else 1 return ParsedTransaction( amount_kopecks=amount_kopecks, description=description, kind=kind, debt_effect=debt_effect, debt_effect_override=-1 if sign == "-" else None, ) def format_money(kopecks: int, *, signed: bool = False) -> str: sign = "" if signed: sign = "+" if kopecks > 0 else ("−" if kopecks < 0 else "") absolute = abs(kopecks) rubles, cents = divmod(absolute, 100) grouped = f"{rubles:,}".replace(",", " ") value = f"{grouped},{cents:02d}" if cents else grouped return f"{sign}{value} ₽" _MONEY_ONLY_RE = re.compile( r"^\s*(?P\d[\d ]*(?:[,.]\d{1,2})?)\s*" r"(?:₽|р\.?|руб(?:лей|ля|ль)?\.?)?\s*$", flags=re.IGNORECASE, ) def parse_money_amount(text: str) -> int: match = _MONEY_ONLY_RE.match(text) if not match: raise ParseError("Некорректная сумма") raw_amount = match.group("amount").replace(" ", "").replace(",", ".") try: amount = Decimal(raw_amount) except InvalidOperation as exc: raise ParseError("Некорректная сумма") from exc if amount < 0: raise ParseError("Сумма не может быть отрицательной") return int((amount * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP))