Repository: humanize (difficulty: medium)

Issue:
intcomma() groups digits incorrectly: it inserts a thousands separator every two digits instead of every three. intcomma(1000) returns '10,00' instead of '1,000', and intcomma(1234567) returns '1,23,45,67' instead of '1,234,567'. The decimal handling, sign handling, and the repeated-substitution loop are all fine -- only the size of each digit group is wrong. Numbers below 1000 (e.g. 100) are unaffected. Fix the grouping so integers are split into groups of three digits from the right.

Source files:
number.py: """Humanizing functions for numbers.

Vendored verbatim from humanize 4.15.0 (number.intcomma), MIT licensed.
"""
from __future__ import annotations


# --- minimal i18n shim (default-locale behavior, stdlib-only) -------------
# Upstream humanize routes user-facing strings through gettext. With no
# locale activated (the default), pgettext/gettext are identity functions
# and the thousands/decimal separators are "," and ".". The function body
# below is copied verbatim from humanize 4.15.0; only this translation layer
# is inlined so the module is self-contained.
def _(message):
    return message


def P_(msgctxt, message):  # _pgettext
    return message


def thousands_separator():
    return ","


def decimal_separator():
    return "."
# -------------------------------------------------------------------------


def _format_not_finite(value: float) -> str:
    """Utility function to handle infinite and nan cases."""
    import math

    if math.isnan(value):
        return "NaN"
    if math.isinf(value) and value < 0:
        return "-Inf"
    if math.isinf(value) and value > 0:
        return "+Inf"
    return ""


def intcomma(value: NumberOrString, ndigits: int | None = None) -> str:
    """Converts an integer to a string containing commas every three digits.

    For example, 3000 becomes "3,000" and 45000 becomes "45,000". To maintain some
    compatibility with Django's `intcomma`, this function also accepts floats.

    Examples:
        ```pycon
        >>> intcomma(100)
        '100'
        >>> intcomma("1000")
        '1,000'
        >>> intcomma(1_000_000)
        '1,000,000'
        >>> intcomma(1_234_567.25)
        '1,234,567.25'
        >>> intcomma(1234.5454545, 2)
        '1,234.55'
        >>> intcomma(14308.40, 1)
        '14,308.4'
        >>> intcomma("14308.40", 1)
        '14,308.4'
        >>> intcomma(None)
        'None'

        ```

    Args:
        value (int, float, str): Integer or float to convert.
        ndigits (int, None): Digits of precision for rounding after the decimal point.

    Returns:
        str: String containing commas every three digits.
    """
    import math

    thousands_sep = thousands_separator()
    decimal_sep = decimal_separator()
    try:
        if isinstance(value, str):
            value = value.replace(thousands_sep, "").replace(decimal_sep, ".")
            if not math.isfinite(float(value)):
                return _format_not_finite(float(value))
            if "." in value:
                value = float(value)
            else:
                value = int(value)
        else:
            if not math.isfinite(float(value)):
                return _format_not_finite(float(value))
            float(value)
    except (TypeError, ValueError):
        return str(value)

    if ndigits is not None:
        orig = "{0:.{1}f}".format(value, ndigits)
    else:
        orig = str(value)
    orig = orig.replace(".", decimal_sep)
    import re

    while True:
        new = re.sub(r"^(-?\d+)(\d{2})", rf"\g<1>{thousands_sep}\g<2>", orig)
        if orig == new:
            return new
        orig = new


Test files:
test_number.py: """Number tests. Slice of humanize 4.15.0 test_number.py (intcomma)."""
from __future__ import annotations

import math

import pytest

from number import intcomma


@pytest.mark.parametrize(
    "test_args, expected",
    [
        ([100], "100"),
        ([1000], "1,000"),
        ([10123], "10,123"),
        ([10311], "10,311"),
        ([1_000_000], "1,000,000"),
        ([1_234_567.25], "1,234,567.25"),
        (["100"], "100"),
        (["1000"], "1,000"),
        (["10123"], "10,123"),
        (["1000000"], "1,000,000"),
        (["1234567.1234567"], "1,234,567.1234567"),
        (["1234567.1234567", 0], "1,234,567"),
        (["1234567", 1], "1,234,567.0"),
        ([None], "None"),
        ([14308.40], "14,308.4"),
        ([14308.40, 2], "14,308.40"),
        ([1234.5454545, 2], "1,234.55"),
        ([math.nan], "NaN"),
        ([math.inf], "+Inf"),
        ([-math.inf], "-Inf"),
    ],
)
def test_intcomma(test_args, expected):
    assert intcomma(*test_args) == expected


Patch:
--- a/number.py
+++ b/number.py
@@ -103,7 +103,7 @@
     import re
 
     while True:
-        new = re.sub(r"^(-?\d+)(\d{2})", rf"\g<1>{thousands_sep}\g<2>", orig)
+        new = re.sub(r"^(-?\d+)(\d{3})", rf"\g<1>{thousands_sep}\g<2>", orig)
         if orig == new:
             return new
         orig = new