Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
129 lines
6.1 KiB
Python
129 lines
6.1 KiB
Python
from datetime import date
|
||
from decimal import Decimal as D
|
||
|
||
import pytest
|
||
|
||
from arm_statements.amounts import build_value, parse_amount, sign_form_of
|
||
from arm_statements.loader import Rules
|
||
from arm_statements.model import Cell, SignForm, Source
|
||
|
||
OCR, EXCEL = Source.OCR, Source.EXCEL
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("raw", "amount", "string_value", "recognized"),
|
||
[
|
||
("1 2О5", D("1205"), None, False),
|
||
("42831 1", D("428311"), None, False),
|
||
("(7744536}", D("-7744536"), None, False),
|
||
("211(1 |", None, "211(1 |", False),
|
||
("(Ю4)", None, "(Ю4)", False),
|
||
(": : 2856983 = ■", D("2856983"), None, False),
|
||
("| : 122477", D("122477"), None, False),
|
||
("|122477", D("122477"), None, False),
|
||
("{38378)", None, "{38378)", False),
|
||
("4,200", D("4200"), None, True),
|
||
("1,234,567", D("1234567"), None, True),
|
||
("837.994", D("837994"), None, True),
|
||
("1 234.567", D("1234567"), None, True),
|
||
("4,2", D("4.2"), None, True),
|
||
("1 234,56", D("1234.56"), None, True),
|
||
("1.234,56", D("123456"), None, False),
|
||
("0,125", None, "0,125", False),
|
||
("1 234 567", D("1234567"), None, True),
|
||
("(1 234)", D("-1234"), None, True),
|
||
("−500", D("-500"), None, True),
|
||
("12:34", None, "12:34", False),
|
||
("1 234 руб.", D("1234"), None, False),
|
||
("—", None, None, True),
|
||
("-", None, None, True),
|
||
("", None, None, True),
|
||
("I", None, "I", False),
|
||
],
|
||
)
|
||
def test_parse_amount_ocr(rules: Rules, raw: str, amount: D | None, string_value: str | None, recognized: bool) -> None:
|
||
parsed = parse_amount(raw, OCR, rules)
|
||
assert (parsed.amount, parsed.string_value, parsed.is_recognized) == (amount, string_value, recognized)
|
||
|
||
|
||
def test_comma_depends_on_source(rules: Rules) -> None:
|
||
assert parse_amount("4,200", EXCEL, rules).amount == D("4.20")
|
||
assert parse_amount("1.500", EXCEL, rules).amount == D("1.5")
|
||
|
||
|
||
def test_sign_forms(rules: Rules) -> None:
|
||
assert sign_form_of("(7744536}", rules) is SignForm.PARENTHESES
|
||
assert sign_form_of("-1 234", rules) is SignForm.MINUS
|
||
assert sign_form_of("38378", rules) is SignForm.UNSIGNED
|
||
assert sign_form_of("{38378)", rules) is None
|
||
|
||
|
||
def test_build_value_excel_number_and_out_of_range(rules: Rules) -> None:
|
||
ok = build_value(Cell("106 311", D("106311.456"), 100.0, "Баланс!D27"), 3, EXCEL, rules)
|
||
assert (ok.value.amount, ok.value.is_recognized, ok.out_of_range) == (D("106311.46"), True, False)
|
||
big = build_value(Cell("1E+17", D("123456789012345678"), 100.0, "Баланс!D28"), 3, EXCEL, rules)
|
||
assert (big.value.amount, big.value.string_value, big.value.is_recognized, big.out_of_range) == (
|
||
None,
|
||
"1E+17",
|
||
False,
|
||
True,
|
||
)
|
||
|
||
|
||
def test_build_value_date_missing_multiline_split(rules: Rules) -> None:
|
||
as_date = build_value(Cell("30.09.2025", date(2025, 9, 30), 100.0, "Баланс!D3"), 3, EXCEL, rules).value
|
||
assert (as_date.amount, as_date.string_value, as_date.is_recognized) == (None, "30.09.2025", False)
|
||
missing = build_value(None, 4, OCR, rules).value
|
||
assert (missing.amount, missing.string_value, missing.text, missing.is_recognized) == (None, None, None, True)
|
||
assert missing.confidence == pytest.approx(100.0)
|
||
multi = build_value(Cell("917 775\n2 371 333", None, 100.0, "row:9"), 2, OCR, rules).value
|
||
assert (multi.amount, multi.string_value, multi.is_recognized) == (None, "917 775\n2 371 333", False)
|
||
part = build_value(Cell("917 775", None, 100.0, "row:9#1"), 2, OCR, rules, split_part=True).value
|
||
assert (part.amount, part.is_recognized) == (D("917775"), False)
|
||
dash = build_value(Cell("—", None, 100.0, "row:3"), 2, OCR, rules).value
|
||
assert (dash.amount, dash.string_value, dash.is_recognized, dash.text) == (None, None, True, "—")
|
||
|
||
|
||
@pytest.mark.parametrize("raw", ["1E+30", "Infinity", "NaN", "-Infinity", "10000000000000000"])
|
||
def test_build_value_decimal_out_of_range(rules: Rules, raw: str) -> None:
|
||
built = build_value(Cell(raw, D(raw), 100.0, "Баланс!D1"), 3, EXCEL, rules)
|
||
assert (built.value.amount, built.value.string_value, built.value.is_recognized, built.out_of_range) == (
|
||
None,
|
||
raw,
|
||
False,
|
||
True,
|
||
)
|
||
|
||
|
||
def test_build_value_decimal_empty_text_and_boundary(rules: Rules) -> None:
|
||
empty = build_value(Cell("", D("1E+30"), 100.0, "Баланс!D1"), 3, EXCEL, rules)
|
||
assert (empty.value.string_value, empty.out_of_range) == (None, True)
|
||
edge = build_value(Cell("x", D("9999999999999999"), 100.0, "Баланс!D1"), 3, EXCEL, rules)
|
||
assert (edge.value.amount, edge.out_of_range) == (D("9999999999999999"), False)
|
||
carry = build_value(Cell("x", D("9999999999999999.999"), 100.0, "Баланс!D1"), 3, EXCEL, rules)
|
||
assert (carry.value.amount, carry.out_of_range) == (None, True)
|
||
|
||
|
||
def test_build_value_rounds_half_up(rules: Rules) -> None:
|
||
assert build_value(Cell("0,125", D("0.125"), 100.0, "Баланс!D1"), 3, EXCEL, rules).value.amount == D("0.13")
|
||
assert build_value(Cell("-0,125", D("-0.125"), 100.0, "Баланс!D1"), 3, EXCEL, rules).value.amount == D("-0.13")
|
||
|
||
|
||
def test_sign_inside_parentheses_is_invalid(rules: Rules) -> None:
|
||
parsed = parse_amount("(-5)", OCR, rules)
|
||
assert (parsed.amount, parsed.string_value, parsed.is_recognized, parsed.sign_form) == (None, "(-5)", False, None)
|
||
|
||
|
||
def test_leading_comma_is_edge_trash(rules: Rules) -> None:
|
||
parsed = parse_amount(",5", OCR, rules)
|
||
assert (parsed.amount, parsed.is_recognized) == (D("5"), False)
|
||
|
||
|
||
def test_guess_amount_is_quantized_to_cents(rules: Rules) -> None:
|
||
"""T20.1: «лучшая догадка» квантуется как прочие пути; в контракте — две цифры после точки."""
|
||
parsed = parse_amount("42831 1", Source.OCR, rules)
|
||
assert parsed.amount == D("428311.00") and parsed.amount.as_tuple().exponent == -2 # noqa: PLR2004 — знаков после запятой
|
||
assert format(parsed.amount, "f") == "428311.00"
|
||
negative = parse_amount("(42831 1)", Source.OCR, rules)
|
||
assert negative.amount is not None and format(negative.amount, "f") == "-428311.00"
|