Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
197 lines
7.2 KiB
Python
197 lines
7.2 KiB
Python
"""Отображаемое значение числа по числовому формату ячейки (#42, R-2…R-6): примеры из реальных выгрузок 1С."""
|
||
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
|
||
from src.parser.statements.number_format import FormatResult, display_amount, general_text
|
||
|
||
|
||
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_is_returned_without_range_check() -> None:
|
||
"""P4-4: диапазон numeric(18,2) проверяет пакет (VALUE_OUT_OF_RANGE), адаптер отдаёт показываемое число."""
|
||
assert shown("1" + "0" * 40, "#,##0") == ("value", Decimal("1" + "0" * 40 + ".00"))
|
||
|
||
|
||
def test_long_integers_keep_all_digits() -> None:
|
||
"""P4-4: длинные целые показываются без потери цифр, диапазон проверяет пакет."""
|
||
assert shown("999999999999999", "0") == ("value", Decimal("999999999999999.00"))
|
||
assert shown("9999999999999999", "0") == ("value", Decimal("9999999999999999.00"))
|
||
assert shown("99999999999999999", "0") == ("value", Decimal("99999999999999999.00"))
|
||
|
||
|
||
def test_scaled_huge_value_is_not_range_checked() -> None:
|
||
assert shown("99999999999999999000", "0,") == ("value", Decimal("99999999999999999.00"))
|
||
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"), "(1\u00a0486\u00a0534)")
|
||
|
||
|
||
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)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("raw", "fmt", "value", "text"),
|
||
[
|
||
("106311116.46", "#,##0,", "106311", "106 311"),
|
||
("-646532.4", "#,##0;(#,##0)", "-646532", "(646 532)"),
|
||
("-1500", "#,##0", "-1500", "-1 500"),
|
||
("-1500", "#,##0;#,##0", "-1500", "1 500"),
|
||
("1.5", "0.00", "1.50", "1,50"),
|
||
("0", '#,##0;-#,##0;"-"', None, "-"),
|
||
("12345678901234567", "0", "12345678901234567", "12345678901234567"),
|
||
],
|
||
)
|
||
def test_display_text(raw: str, fmt: str, value: str | None, text: str) -> None:
|
||
shown = display_amount(Decimal(raw), fmt)
|
||
expected = None if value is None else Decimal(value)
|
||
assert (shown.kind, shown.value, shown.text) == ("value", expected, text)
|
||
|
||
|
||
def test_general_text() -> None:
|
||
texts = [general_text(Decimal(x)) for x in ("7016006635.0", "106311116.460", "-0.5")]
|
||
assert texts == ["7016006635", "106311116,46", "-0,5"]
|