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, ) def parse_transaction(text: str) -> ParsedTransaction: match = _TRANSACTION_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("Сумма должна быть больше нуля") amount_kopecks = int((amount * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)) description = " ".join(match.group("description").split()) kind_word = (match.group("kind") or "").casefold() sign = match.group("sign") kind = ( TransactionKind.INCOME if sign == "+" or kind_word in {"доход", "приход"} else TransactionKind.EXPENSE ) if sign == "-" and kind is TransactionKind.INCOME: raise ParseError("Доход не может быть отрицательным") return ParsedTransaction( amount_kopecks=amount_kopecks, description=description, kind=kind, ) 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))