From 5f259be9ace0d2503f8215b662c7a59ebad5d3bb Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 18 Oct 2026 13:28:21 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20out-of-range=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BB=D1=8E=D0=B1=D1=8B=D1=85=20Decimal,=20=D0=B7=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=B2=20=D1=81=D0=BA=D0=BE=D0=B1=D0=BA=D0=B0=D1=85=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=B4=D0=BE=D0=BF=D1=83=D1=81=D1=82=D0=B8=D0=BC,?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BE=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B3=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W --- arm_statements/amounts.py | 12 ++++------ arm_statements/tests/test_amounts.py | 35 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/arm_statements/amounts.py b/arm_statements/amounts.py index 1c40eac..208eab6 100644 --- a/arm_statements/amounts.py +++ b/arm_statements/amounts.py @@ -88,8 +88,6 @@ def _split_core(core: str) -> tuple[str, SignForm, bool, bool] | None: fixed = core[-1] != ")" core = core[1:-1].strip() sign_form, negative = SignForm.PARENTHESES, True - if core and core[0] in "-−+": - core = core[1:].strip() elif core[0] in "-−+": if core[0] in _MINUSES: sign_form, negative = SignForm.MINUS, True @@ -118,8 +116,6 @@ def _number(body: str, source: Source, text: str, sign_form: SignForm, *, negati return _guess(digits, text, sign_form, negative=negative) else: integer, fraction = compact.replace(",", ".").split(".") - if not integer: - return AmountParse(None, text, False, sign_form) if source is Source.OCR and len(fraction) >= 3: # noqa: PLR2004 — три знака: не копейки return AmountParse(None, text, False, sign_form) if len(integer) > MAX_INTEGER_DIGITS: @@ -181,9 +177,11 @@ def build_value( string_text = text if text.strip() else None value = cell.value if isinstance(value, Decimal): - amount = _quantize(value) - if _too_long(amount): - return ValueBuild(_plain(cell, column_index, AmountParse(None, text, False, None)), out_of_range=True) + # длину целой части проверяем до квантования: для 1E+30 и нефинитных значений quantize падает + amount = _quantize(value) if value.is_finite() and value.adjusted() < MAX_INTEGER_DIGITS else None + if amount is None or _too_long(amount): + overflow = AmountParse(None, string_text, False, None) + return ValueBuild(_plain(cell, column_index, overflow), out_of_range=True) return ValueBuild( _plain(cell, column_index, AmountParse(amount, None, True, None), recognized=not split_part), out_of_range=False, diff --git a/arm_statements/tests/test_amounts.py b/arm_statements/tests/test_amounts.py index 8025cf5..859880f 100644 --- a/arm_statements/tests/test_amounts.py +++ b/arm_statements/tests/test_amounts.py @@ -82,3 +82,38 @@ def test_build_value_date_missing_multiline_split(rules: Rules) -> None: 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)