Skip to content
26 changes: 17 additions & 9 deletions src/humanize/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,43 @@

TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterable
from typing import Any

__all__ = ["natural_list"]


def natural_list(items: list[Any]) -> str:
def natural_list(items: Iterable[Any], oxford_comma: bool = False) -> str:
"""Natural list.

Convert a list of items into a human-readable string with commas and 'and'.
Convert a list or iterable of items into a human-readable string with
commas and 'and'.

Examples:
>>> natural_list(["one", "two", "three"])
'one, two and three'
>>> natural_list(["one", "two", "three"], oxford_comma=True)
'one, two, and three'
>>> natural_list(["one", "two"])
'one and two'
>>> natural_list(["one"])
'one'

Args:
items (list): An iterable of items.
items (iterable): An iterable of items.
oxford_comma (bool): If True, includes an Oxford comma before 'and'
for 3+ items.

Returns:
str: A string with commas and 'and' in the right places.
"""
if not items:
item_list = [str(item) for item in items]
if not item_list:
return ""
if len(items) == 1:
return str(items[0])
elif len(items) == 2:
return f"{str(items[0])} and {str(items[1])}"
if len(item_list) == 1:
return item_list[0]
elif len(item_list) == 2:
return f"{item_list[0]} and {item_list[1]}"
else:
return ", ".join([str(item) for item in items[:-1]]) + f" and {str(items[-1])}"
sep = ", and " if oxford_comma else " and "
return ", ".join(item_list[:-1]) + f"{sep}{item_list[-1]}"
3 changes: 2 additions & 1 deletion src/humanize/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ def ordinal(value: NumberOrString, gender: str = "male") -> str:
except (TypeError, ValueError):
return str(value)
gender = "male" if gender == "male" else "female"
digit = 0 if value % 100 in (11, 12, 13) else value % 10
abs_value = abs(value)
digit = 0 if abs_value % 100 in (11, 12, 13) else abs_value % 10
return f"{value}{P_(f'{digit} ({gender})', _ORDINAL_SUFFIXES[digit])}"


Expand Down
2 changes: 2 additions & 0 deletions tests/test_benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import pytest

pytest.importorskip("pytest_codspeed")

import humanize

TYPE_CHECKING = False
Expand Down
4 changes: 3 additions & 1 deletion tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import importlib

import pytest
from freezegun import freeze_time

import humanize

freezegun = pytest.importorskip("freezegun")
freeze_time = freezegun.freeze_time

with freeze_time("2020-02-02"):
NOW = dt.datetime.now(tz=dt.timezone.utc)

Expand Down
11 changes: 11 additions & 0 deletions tests/test_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@
([[""]], ""),
([[1, 2, 3]], "1, 2 and 3"),
([[1, "two"]], "1 and two"),
([("a", "b", "c")], "a, b and c"),
],
)
def test_natural_list(
test_args: list[str] | list[int] | list[str | int], expected: str
) -> None:
assert humanize.natural_list(*test_args) == expected


def test_natural_list_generator_and_oxford_comma() -> None:
gen = (x for x in ["alpha", "beta", "gamma"])
assert humanize.natural_list(gen) == "alpha, beta and gamma"
assert (
humanize.natural_list(["one", "two", "three"], oxford_comma=True)
== "one, two, and three"
)
assert humanize.natural_list(["one", "two"], oxford_comma=True) == "one and two"
6 changes: 6 additions & 0 deletions tests/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
("102", "102nd"),
("103", "103rd"),
("111", "111th"),
("-1", "-1st"),
("-2", "-2nd"),
("-3", "-3rd"),
("-11", "-11th"),
("-21", "-21st"),
("-22", "-22nd"),
("something else", "something else"),
(None, "None"),
(math.nan, "NaN"),
Expand Down
4 changes: 3 additions & 1 deletion tests/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import typing

import pytest
from freezegun import freeze_time

import humanize
from humanize import time

freezegun = pytest.importorskip("freezegun")
freeze_time = freezegun.freeze_time

ONE_DAY_DELTA = dt.timedelta(days=1)

# In seconds
Expand Down