diff --git a/src/humanize/number.py b/src/humanize/number.py index 52a5356a..ac16d733 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -184,7 +184,27 @@ 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) + # Without an explicit rounding request, grouping a decimal + # string must not lose digits to binary float rounding + # (issue #496: '1234567890123456.78' became '...456.8'). + # Parse plain decimal strings exactly, as the Decimal input + # path already does; anything else (exponents, trailing + # dots) keeps the previous float behaviour. + parts = value.lstrip("+-").split(".") + digits = "0123456789" + if ( + ndigits is None + and len(parts) == 2 + and all(c in digits for c in parts[0]) + and parts[0] + and all(c in digits for c in parts[1]) + and parts[1] + ): + from decimal import Decimal + + value = Decimal(value) + else: + value = float(value) else: value = int(value) elif not isinstance(value, int): diff --git a/tests/test_number.py b/tests/test_number.py index 5fb12fa6..1004e106 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -53,6 +53,11 @@ def test_ordinal(test_input: str, expected: str) -> None: (["10311"], "10,311"), (["1000000"], "1,000,000"), (["1234567.1234567"], "1,234,567.1234567"), + # Regression for issue #496: grouping a large decimal string must + # not lose digits to binary float rounding. + (["1234567890123456.78"], "1,234,567,890,123,456.78"), + (["-1234567890123456.78"], "-1,234,567,890,123,456.78"), + (["14308.40"], "14,308.40"), (["1234567.1234567", 0], "1,234,567"), (["1234567.1234567", 1], "1,234,567.1"), (["1234567.1234567", 10], "1,234,567.1234567000"),