diff --git a/src/humanize/number.py b/src/humanize/number.py index 52a5356a..99d3a4d1 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -145,6 +145,8 @@ def intcomma(value: NumberOrString, ndigits: int | None = None) -> str: For example, 3000 becomes "3,000" and 45000 becomes "45,000". To maintain some compatibility with Django's `intcomma`, this function also accepts floats. + Decimal strings without exponent notation retain their precision when + `ndigits` is not specified. Examples: ```pycon @@ -176,6 +178,7 @@ def intcomma(value: NumberOrString, ndigits: int | None = None) -> str: """ import math + decimal_value = None thousands_sep = thousands_separator() decimal_sep = decimal_separator() try: @@ -184,7 +187,12 @@ def intcomma(value: NumberOrString, ndigits: int | None = None) -> str: if not math.isfinite(float(value)): return _format_not_finite(float(value)) if "." in value: - value = float(value) + if ndigits is None and "e" not in value.lower(): + from decimal import Decimal + + decimal_value = Decimal(value) + else: + value = float(value) else: value = int(value) elif not isinstance(value, int): @@ -196,6 +204,8 @@ def intcomma(value: NumberOrString, ndigits: int | None = None) -> str: if ndigits is not None: result = f"{value:,.{ndigits}f}" + elif decimal_value is not None: + result = f"{decimal_value:,f}" else: result = f"{value:,}" if thousands_sep != "," or decimal_sep != ".": diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 0d3ced84..5754a53a 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -140,11 +140,19 @@ def test_intcomma() -> None: assert humanize.intcomma("1234567,89") == "1.234.567,89" assert humanize.intcomma("1.234.567,89") == "1.234.567,89" assert humanize.intcomma("1.234.567,8") == "1.234.567,8" + assert ( + humanize.intcomma("1.234.567.890.123.456,78") == "1.234.567.890.123.456,78" + ) + assert humanize.intcomma("-1234567890123456,78") == "-1.234.567.890.123.456,78" + assert humanize.intcomma("1234,1200") == "1.234,1200" humanize.i18n.activate("fr_FR") assert humanize.intcomma(number) == "10 000 000" assert humanize.intcomma(1_234_567.89) == "1 234 567,89" assert humanize.intcomma("1 234 567,89") == "1 234 567,89" + assert ( + humanize.intcomma("1 234 567 890 123 456,78") == "1 234 567 890 123 456,78" + ) humanize.i18n.activate("pt_BR") assert humanize.intcomma(number) == "10.000.000" diff --git a/tests/test_number.py b/tests/test_number.py index 5fb12fa6..88af55cf 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -83,6 +83,41 @@ def test_intcomma( assert humanize.intcomma(*test_args) == expected +@pytest.mark.parametrize( + "value, expected", + [ + ("1234567890123456.78", "1,234,567,890,123,456.78"), + ("-1234567890123456.78", "-1,234,567,890,123,456.78"), + ("0.1234567890123456789", "0.1234567890123456789"), + ("0.000000000000000000123456789", "0.000000000000000000123456789"), + ("1234.1200", "1,234.1200"), + ("+001234.1200", "1,234.1200"), + ], +) +def test_intcomma_decimal_string_precision(value: str, expected: str) -> None: + assert humanize.intcomma(value) == expected + + +@pytest.mark.parametrize( + "value, ndigits, expected", + [ + ("1234.545", 2, "1,234.55"), + ("1234.1200", 3, "1,234.120"), + ("1.0e3", None, "1,000.0"), + ("1e3", None, "1e3"), + ("bad.12", None, "bad.12"), + ("1.2.3", None, "1.2.3"), + ("nan", None, "NaN"), + ("-inf", None, "-Inf"), + ("1.0e400", None, "+Inf"), + ], +) +def test_intcomma_decimal_string_controls( + value: str, ndigits: int | None, expected: str +) -> None: + assert humanize.intcomma(value, ndigits) == expected + + @pytest.mark.parametrize("sign", [1, -1]) def test_intcomma_large_integer(sign: int) -> None: value = sign * (10**400 + 123)