Repository: python-humanize/humanize (language: python)

Issue:
Feature: Natural list
I was wondering if we can have a feature that converts a list to a sentence; example - `["dog", "cat", "mouse"]` and convert it to `Dog, Cat and Mouse`.

I can add this feature if you are up for it. Thanks.


Discussion / hints:
Thanks for the suggestion.

That would be useful, and could be implemented along the lines of https://stackoverflow.com/a/32008908/724176

Let's leave the capitalisation out. 

We could also have an input parameter to choose whether to use Oxford commas (a, b, and c) or AP style (a, b and c). And maybe to also change the "and" to "or" or something else. But let's defer those for followups if there's actual demand for it. 


Yeah that's what I have been using so far.

Fix patch:
diff --git a/.gitignore b/.gitignore
index bddede8..7ab546c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -152,12 +152,8 @@ dmypy.json
 # Cython debug symbols
 cython_debug/
 
-# PyCharm
-#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-#  and can be added to the global gitignore or merged into this file.  For a more nuclear
-#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+# Jetbrains IDEs
+.idea/
 
 # hatch-vcs
 src/*/_version.py
diff --git a/docs/index.md b/docs/index.md
index 80d4bb2..e1c002b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,6 +6,7 @@ Welcome to the humanize API reference.
 - [Time](time.md)
 - [Filesize](filesize.md)
 - [I18n](i18n.md)
+- [Lists](lists.md)
 
 {%
    include-markdown "../README.md"
diff --git a/docs/lists.md b/docs/lists.md
new file mode 100644
index 0000000..eab0e08
--- /dev/null
+++ b/docs/lists.md
@@ -0,0 +1,3 @@
+# Lists
+
+::: humanize.lists
diff --git a/mkdocs.yml b/mkdocs.yml
index e818401..8605493 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -21,6 +21,7 @@ nav:
   - Time: time.md
   - Filesize: filesize.md
   - Internationalisation: i18n.md
+  - Lists: lists.md
 
 plugins:
   - search
diff --git a/src/humanize/__init__.py b/src/humanize/__init__.py
index 60390e2..224c483 100644
--- a/src/humanize/__init__.py
+++ b/src/humanize/__init__.py
@@ -4,6 +4,7 @@
 
 from humanize.filesize import naturalsize
 from humanize.i18n import activate, deactivate, decimal_separator, thousands_separator
+from humanize.lists import natural_list
 from humanize.number import (
     apnumber,
     clamp,
@@ -38,6 +39,7 @@
     "naturaldate",
     "naturalday",
     "naturaldelta",
+    "natural_list",
     "naturalsize",
     "naturaltime",
     "ordinal",
diff --git a/src/humanize/lists.py b/src/humanize/lists.py
new file mode 100644
index 0000000..1573be8
--- /dev/null
+++ b/src/humanize/lists.py
@@ -0,0 +1,34 @@
+"""Lists related humanization."""
+
+from __future__ import annotations
+
+from typing import Any
+
+__all__ = ["natural_list"]
+
+
+def natural_list(items: list[Any]) -> str:
+    """Natural list.
+
+    Convert a list 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"])
+        'one and two'
+        >>> natural_list(["one"])
+        'one'
+
+    Args:
+        items (list): An iterable of items.
+
+    Returns:
+        str: A string with commas and 'and' in the right places.
+    """
+    if len(items) == 1:
+        return str(items[0])
+    elif len(items) == 2:
+        return f"{str(items[0])} and {str(items[1])}"
+    else:
+        return ", ".join(str(item) for item in items[:-1]) + f" and {str(items[-1])}"


Test patch:
diff --git a/tests/test_lists.py b/tests/test_lists.py
new file mode 100644
index 0000000..a39d344
--- /dev/null
+++ b/tests/test_lists.py
@@ -0,0 +1,23 @@
+from __future__ import annotations
+
+import pytest
+
+import humanize
+
+
+@pytest.mark.parametrize(
+    "test_args, expected",
+    [
+        ([["1", "2", "3"]], "1, 2 and 3"),
+        ([["one", "two", "three"]], "one, two and three"),
+        ([["one", "two"]], "one and two"),
+        ([["one"]], "one"),
+        ([[""]], ""),
+        ([[1, 2, 3]], "1, 2 and 3"),
+        ([[1, "two"]], "1 and two"),
+    ],
+)
+def test_natural_list(
+    test_args: list[str] | list[int] | list[str | int], expected: str
+) -> None:
+    assert humanize.natural_list(*test_args) == expected