Repository: ul-mds/gecko

Issue:
Add functions for converting to lower- and uppercase

Discussion:

Solution patch:
diff --git a/docs/data-mutation.md b/docs/data-mutation.md
index 21d33c7..529d498 100644
--- a/docs/data-mutation.md
+++ b/docs/data-mutation.md
@@ -354,6 +354,28 @@ print(replacement_mutator([srs]))
 # => ["lcick 0", "step |", "go z", "run s"]
 ```
 
+### Case conversions
+
+During data entry or normalization, it may occur that text is converted to all lowercase or uppercase, by accident or on
+purpose.
+`with_lowercase` and `with_uppercase` handle these use cases.
+
+```python
+import pandas as pd
+
+from gecko import mutator
+
+srs = pd.Series(["Foobar", "foobaz", "FOOBAT"])
+
+lowercase_mutator = mutator.with_lowercase()
+uppercase_mutator = mutator.with_uppercase()
+
+print(lowercase_mutator([srs]))
+# => ["foobar", "foobaz", "foobat"]
+print(uppercase_mutator([srs]))
+# => ["FOOBAR", "FOOBAZ", "FOOBAT"]
+```
+
 ## Multiple mutators
 
 Using `mutate_data_frame`, you can apply multiple mutators on many columns at once.
diff --git a/gecko/mutator.py b/gecko/mutator.py
index f1c25a1..09f14b5 100644
--- a/gecko/mutator.py
+++ b/gecko/mutator.py
@@ -18,6 +18,8 @@
     "with_categorical_values",
     "with_function",
     "with_permute",
+    "with_lowercase",
+    "with_uppercase",
     "mutate_data_frame",
 ]
 
@@ -1182,6 +1184,34 @@ def _mutate(srs_lst: list[pd.Series]) -> list[pd.Series]:
     return _mutate
 
 
+def with_lowercase() -> Mutator:
+    """
+    Mutate data from a series by converting it into lowercase.
+
+    Returns:
+        function returning list with the entries in each series converted to lowercase
+    """
+
+    def _mutate(srs_lst: list[pd.Series]) -> list[pd.Series]:
+        return [srs.str.lower() for srs in srs_lst]
+
+    return _mutate
+
+
+def with_uppercase() -> Mutator:
+    """
+    Mutate data from a series by converting it into uppercase.
+
+    Returns:
+        function returning list with the entries in each series converted to uppercase
+    """
+
+    def _mutate(srs_lst: list[pd.Series]) -> list[pd.Series]:
+        return [srs.str.upper() for srs in srs_lst]
+
+    return _mutate
+
+
 def mutate_data_frame(
     df_in: pd.DataFrame,
     column_to_mutator_list: list[


Test patch:
diff --git a/tests/test_mutator.py b/tests/test_mutator.py
index 76e6606..9db9efc 100644
--- a/tests/test_mutator.py
+++ b/tests/test_mutator.py
@@ -20,6 +20,8 @@
     with_function,
     with_permute,
     Mutator,
+    with_lowercase,
+    with_uppercase,
 )
 from tests.helpers import get_asset_path
 
@@ -322,6 +324,26 @@ def test_permute_more_than_two(rng):
             assert (orig_srs[i] == mut_srs[j]).any()
 
 
+def test_with_lowercase():
+    srs = pd.Series(["Foobar"] * 100)
+    mutate_lowercase = with_lowercase()
+    (srs_mutated,) = mutate_lowercase([srs])
+
+    assert len(srs) == len(srs_mutated)
+    assert (srs != srs_mutated).all()
+    assert (srs_mutated == "foobar").all()
+
+
+def test_with_uppercase():
+    srs = pd.Series(["Foobar"] * 100)
+    mutate_uppercase = with_uppercase()
+    (srs_mutated,) = mutate_uppercase([srs])
+
+    assert len(srs) == len(srs_mutated)
+    assert (srs != srs_mutated).all()
+    assert (srs_mutated == "FOOBAR").all()
+
+
 def test_mutate_data_frame_single(rng):
     df = pd.DataFrame({"foo": list(string.ascii_letters)})
     df_mut = mutate_data_frame(
@@ -514,6 +536,8 @@ def test_mutate_data_frame_multicolumn_noop():
         ),
         (1, with_replacement_table(get_asset_path("ocr.csv"), rng=__dummy_rng)),
         (2, with_permute()),
+        (1, with_lowercase()),
+        (1, with_uppercase()),
     ],
 )
 def test_mutator_no_modify(num_srs: int, func: Mutator, rng):