Andrey Epifancev 904392ddcb number_format: 15 значащих цифр с округлением вверх, неразобранные условия и >4 секций; признак xlsb по содержимому (#42)
Фикс-волна по финальному ревью: I-1, M-1, M-4, M-5, M-6 (T4.1, T4.2); тесты на ;;;, 5 секций, [$-419], \(…\), [=5]0;[=5]0,.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:46:11 +04:00

173 lines
6.0 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Отображаемое значение числа по числовому формату ячейки (#42, R-2…R-6): примеры из реальных выгрузок 1С."""
from decimal import Decimal
import pytest
from src.parser.statements.number_format import FormatResult, display_amount
def shown(raw: str, fmt: str) -> tuple[str, Decimal | None]:
result = display_amount(Decimal(raw), fmt)
return result.kind, result.value
@pytest.mark.parametrize(
("raw", "fmt", "expected"),
[
("10741981390", "#,##0,", "10741981.00"),
("586260.26", "0,", "586.00"),
("898852.21795", "#,##0", "898852.00"),
("898852.5", "#,##0", "898853.00"),
("1234.5", "#,##0.00", "1234.50"),
("1234.567", "#,##0.00", "1234.57"),
("1500", "#,##0,", "2.00"),
("-1500", "#,##0,", "-2.00"),
("1499.99", "#,##0,", "1.00"),
("-1499.99", "#,##0,", "-1.00"),
("586260.26", "0.00,", "586.26"),
("5000000", "#,##0,,", "5.00"),
("1500", '#,##0,;(#,##0,);"-"', "2.00"),
("-1500", '#,##0,;(#,##0,);"-"', "-2.00"),
("1500", "#,##0;(#,##0)", "1500.00"),
("-1500", "#,##0;(#,##0)", "-1500.00"),
("-1500", "#,##0;#,##0", "-1500.00"),
("1500", "[Red]#,##0", "1500.00"),
("1500", "[$-419]#,##0", "1500.00"),
("1500", "#,##0_);[Red](#,##0)", "1500.00"),
("-1500", "#,##0_);[Red](#,##0)", "-1500.00"),
("1500.126", "0.000", "1500.13"),
("1500", '0 "тыс."', "1500.00"),
("-872206880.07", '[=-872206880.07]"(872 207)";#,##0,', "-872207.00"),
("-872206881", '[=-872206880.07]"(872 207)";#,##0,', "-872207.00"),
("-646532", '[=-646532]"(647)"', "-647.00"),
("12345678", '[=-872206880.07]"(872 207)";#,##0,', "12346.00"),
("1500499.9999999998", "#,##0,", "1501.00"),
("1500", "[$-419]#,##0,", "2.00"),
("1500", "\\(#,##0\\)", "1500.00"),
("5", "[=5]0;[=5]0,", "5.00"),
("-1500", "[$-419]#,##0,", "-2.00"),
],
)
def test_value(raw: str, fmt: str, expected: str) -> None:
kind, value = shown(raw, fmt)
assert kind == "value"
assert value is not None
assert str(value) == expected
@pytest.mark.parametrize(
("raw", "fmt"),
[
("0", '[=0]"-"'),
("0", '[=0]""'),
("0", '[=0]"-";General'),
("-0.79", '[=-0.79]"-"'),
("0", '#,##0,;(#,##0,);"-"'),
("0", ";;;"),
("7", ";;;"),
],
)
def test_no_value(raw: str, fmt: str) -> None:
assert shown(raw, fmt) == ("value", None)
@pytest.mark.parametrize(
("raw", "fmt"),
[
("5", '[=0]"-"'),
("5", "0%"),
("5", "0.00E+00"),
("5", "# ?/?"),
("5", "@"),
("5", "dd.mm.yyyy"),
("5", "h:mm AM/PM"),
("0", '[=0]"н/д"'),
("5", '"н/д"'),
("5", "0 руб"),
("5", "[=1E-5]0"),
("5", "[=1,5]0"),
("5", "0 ₽"),
("5", "0;0;0;@;0"),
],
)
def test_unparsed(raw: str, fmt: str) -> None:
assert shown(raw, fmt) == ("unparsed", None)
@pytest.mark.parametrize("fmt", ["General", "general", "", " "])
def test_raw(fmt: str) -> None:
assert shown("5.5", fmt) == ("raw", None)
def test_general_section_after_condition() -> None:
assert shown("5.5", '[=0]"-";General') == ("raw", None)
@pytest.mark.parametrize(
("raw", "fmt", "expected"),
[
("5", "[<10]0;[>=10]0,", "5.00"),
("20000", "[<10]0;[>=10]0,", "20.00"),
("5", "[<>5]0;0.0", "5.0"),
],
)
def test_comparison_conditions(raw: str, fmt: str, expected: str) -> None:
kind, value = shown(raw, fmt)
assert kind == "value"
assert value is not None
assert Decimal(expected) == value
def test_fourth_section_ignored() -> None:
assert shown("1500", "#,##0,;(#,##0,);0;@") == ("value", Decimal("2.00"))
def test_semicolon_in_quotes_does_not_split() -> None:
assert shown("0", '[=0]"a;b"') == ("unparsed", None)
def test_escaped_char_is_decoration() -> None:
assert shown("1500", "#,##0\\ \\р") == ("value", Decimal("1500.00"))
def test_huge_value_out_of_range_is_unparsed() -> None:
assert shown("1" + "0" * 40, "#,##0") == ("unparsed", None)
def test_max_numeric_18_2_range() -> None:
"""Excel показывает 15 значащих цифр: 16-значное целое округляется до 1E+16, что уже вне numeric(18,2)."""
assert shown("999999999999999", "0") == ("value", Decimal("999999999999999.00"))
assert shown("9999999999999999", "0") == ("unparsed", None)
assert shown("99999999999999999", "0") == ("unparsed", None)
def test_scaled_huge_value_fits() -> None:
assert shown("99999999999999999000", "0,") == ("unparsed", None)
assert shown("999999999999999000", "0,") == ("value", Decimal("999999999999999.00"))
def test_condition_compares_with_excel_15_digit_precision() -> None:
"""Из xls значение приходит как -1486533582.3500001, условие записано `-1486533582.35`: Excel считает их равными."""
fmt = '[=-1486533582.35]"(1\u00a0486\u00a0534)"'
result = display_amount(Decimal("-1486533582.3500001"), fmt)
assert result == FormatResult("value", Decimal("-1486534"))
def test_value_is_rounded_to_15_digits_half_up_before_scaling() -> None:
"""Артефакт float 1500499.9999999998 в Excel — 1500500, после деления на 1000 половина округляется вверх."""
assert shown("1500499.9999999998", "#,##0,") == ("value", Decimal("1501"))
def test_condition_operand_is_rounded_to_15_digits() -> None:
"""Литерал «-» — «нет значения»; «x» дал бы unparsed по R-5 независимо от условия, поэтому берём прочерк."""
result = display_amount(Decimal("0.1234567890123456"), '[=0.1234567890123456]"-"')
assert result == FormatResult("value", None)
def test_condition_that_does_not_match_the_pattern_is_unparsed() -> None:
assert shown("0", '[=1E-5]"x"') == ("unparsed", None)