Repository: humanize (difficulty: medium)

Issue:
ordinal() produces the wrong suffix for numbers ending in 13. ordinal(13) returns '13rd' and ordinal(113) returns '113rd', but in English the numbers 11, 12 and 13 are all special 'teens' that take the 'th' suffix -- the correct results are '13th' and '113th'. ordinal(11) and ordinal(12) are already correct, and numbers like 3, 103, 23 (ending in 3 but not in the 11-13 teen range) should still get 'rd'. The teens special-case is just missing one of its members. Make ordinal(13), ordinal(113), ordinal(213)... return a 'th' suffix while leaving every other case unchanged.

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

Vendored verbatim from humanize 4.15.0 (number.ordinal), 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 ordinal(value: NumberOrString, gender: str = "male") -> str:
    """Converts an integer to its ordinal as a string.

    For example, 1 is "1st", 2 is "2nd", 3 is "3rd", etc. Works for any integer or
    anything `int()` will turn into an integer. Anything else will return the output
    of str(value).

    Examples:
        ```pycon
        >>> ordinal(1)
        '1st'
        >>> ordinal(1002)
        '1002nd'
        >>> ordinal(103)
        '103rd'
        >>> ordinal(4)
        '4th'
        >>> ordinal(12)
        '12th'
        >>> ordinal(101)
        '101st'
        >>> ordinal(111)
        '111th'
        >>> ordinal("something else")
        'something else'
        >>> ordinal([1, 2, 3]) == "[1, 2, 3]"
        True

        ```

    Args:
        value (int, str, float): Integer to convert.
        gender (str): Gender for translations. Accepts either "male" or "female".

    Returns:
        str: Ordinal string.
    """
    import math

    try:
        if not math.isfinite(float(value)):
            return _format_not_finite(float(value))
        value = int(value)
    except (TypeError, ValueError):
        return str(value)
    if gender == "male":
        t = (
            P_("0 (male)", "th"),
            P_("1 (male)", "st"),
            P_("2 (male)", "nd"),
            P_("3 (male)", "rd"),
            P_("4 (male)", "th"),
            P_("5 (male)", "th"),
            P_("6 (male)", "th"),
            P_("7 (male)", "th"),
            P_("8 (male)", "th"),
            P_("9 (male)", "th"),
        )
    else:
        t = (
            P_("0 (female)", "th"),
            P_("1 (female)", "st"),
            P_("2 (female)", "nd"),
            P_("3 (female)", "rd"),
            P_("4 (female)", "th"),
            P_("5 (female)", "th"),
            P_("6 (female)", "th"),
            P_("7 (female)", "th"),
            P_("8 (female)", "th"),
            P_("9 (female)", "th"),
        )
    if value % 100 in (11, 12):  # special case
        return f"{value}{t[0]}"
    return f"{value}{t[value % 10]}"


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

import math

import pytest

from number import ordinal


@pytest.mark.parametrize(
    "test_input, expected",
    [
        ("1", "1st"),
        ("2", "2nd"),
        ("3", "3rd"),
        ("4", "4th"),
        ("11", "11th"),
        ("12", "12th"),
        ("13", "13th"),
        ("101", "101st"),
        ("102", "102nd"),
        ("103", "103rd"),
        ("111", "111th"),
        ("112", "112th"),
        ("113", "113th"),
        ("something else", "something else"),
        (None, "None"),
        (math.nan, "NaN"),
        (math.inf, "+Inf"),
        (-math.inf, "-Inf"),
        ("nan", "NaN"),
        ("-inf", "-Inf"),
    ],
)
def test_ordinal(test_input, expected):
    assert ordinal(test_input) == expected


Patch:
--- a/number.py
+++ b/number.py
@@ -112,6 +112,6 @@
             P_("8 (female)", "th"),
             P_("9 (female)", "th"),
         )
-    if value % 100 in (11, 12):  # special case
+    if value % 100 in (11, 12, 13):  # special case
         return f"{value}{t[0]}"
     return f"{value}{t[value % 10]}"