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)