From 226a9a47bf13865645d7e776999fa986af9a33fe Mon Sep 17 00:00:00 2001 From: Vinayak Deshmuk <118991845+Vinayak19112003@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:06:41 +0530 Subject: [PATCH] Fix intcomma() returning scientific notation for large floats Closes #431 --- src/humanize/number.py | 17 +++++++++++++++++ tests/test_number.py | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/humanize/number.py b/src/humanize/number.py index 52a5356a..f35a8ec8 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -196,6 +196,23 @@ def intcomma(value: NumberOrString, ndigits: int | None = None) -> str: if ndigits is not None: result = f"{value:,.{ndigits}f}" + elif isinstance(value, float): + # f"{value:,}" formats floats via repr(), which switches to scientific + # notation at 1e16 (and for tiny floats like 1e-5), so no thousands + # separators are added. Expand to fixed-point first (see #431). + import decimal + + expanded = f"{decimal.Decimal(repr(value)):f}" + if "." in expanded: + integer_part, fraction_part = expanded.split(".") + else: + integer_part, fraction_part = expanded, "0" + if integer_part.startswith("-"): + sign, integer_part = "-", integer_part[1:] + else: + sign = "" + grouped = f"{int(integer_part):,}" if integer_part else "0" + result = f"{sign}{grouped}.{fraction_part}" else: result = f"{value:,}" if thousands_sep != "," or decimal_sep != ".": diff --git a/tests/test_number.py b/tests/test_number.py index 5fb12fa6..25201cfb 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -90,6 +90,27 @@ def test_intcomma_large_integer(sign: int) -> None: assert humanize.intcomma(value) == expected +@pytest.mark.parametrize( + "test_args, expected", + [ + ([1e15], "1,000,000,000,000,000.0"), + ([1e16], "10,000,000,000,000,000.0"), + ([1.5e20], "150,000,000,000,000,000,000.0"), + ([-1e16], "-10,000,000,000,000,000.0"), + ([0.00001], "0.00001"), + ([1e16, 0], "10,000,000,000,000,000"), + ], + ids=str, +) +def test_intcomma_large_float_no_scientific_notation( + test_args: list[typing.Any], expected: str +) -> None: + # https://github.com/python-humanize/humanize/issues/431 + # Floats at 1e16 and above (and tiny floats like 1e-5) were rendered in + # scientific notation with no thousands separators. + assert humanize.intcomma(*test_args) == expected + + def test_intword_powers() -> None: # make sure that powers & human_powers have the same number of items assert len(number.powers) == len(number.human_powers)