Repository: narwhals-dev/narwhals
Pull request #1292 (2024-10-31 10:08:40)

Enh: adding automated inferencing of format `%Y-%m-%dT%H:%M` in pyarrow
<!--
# Thanks for contributing a pull request! 
## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] ✨ Feature
- [ ] 🐛 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📝 Documentation
- [ ] ✅ Test
- [ ] 🐳 Other

## Related issues 

- Related issue #1282
- Closes #

## Checklist

- [x] Code follows style guide (ruff)
- [x] Tests added 
- [ ] Documented the changes

## If you have comments or can explain your changes, please do so below.



MarcoGorelli: awesome!

@FBruzzesi fancy taking a look if you have time? (not urgent of course)

Patch:
diff --git a/narwhals/_arrow/utils.py b/narwhals/_arrow/utils.py
index bc8d1701..6f74294d 100644
--- a/narwhals/_arrow/utils.py
+++ b/narwhals/_arrow/utils.py
@@ -340,7 +340,9 @@ def convert_str_slice_to_int_slice(
 # Regex for date, time, separator and timezone components
 DATE_RE = r"(?P<date>\d{1,4}[-/.]\d{1,2}[-/.]\d{1,4})"
 SEP_RE = r"(?P<sep>\s|T)"
-TIME_RE = r"(?P<time>\d{2}:\d{2}:\d{2})"  # \s*(?P<period>[AP]M)?)?
+TIME_RE = r"(?P<time>\d{2}:\d{2}(?::\d{2})?)"  # \s*(?P<period>[AP]M)?)?
+HMS_RE = r"^(?P<hms>\d{2}:\d{2}:\d{2})$"
+HM_RE = r"^(?P<hm>\d{2}:\d{2})$"
 TZ_RE = r"(?P<tz>Z|[+-]\d{2}:?\d{2})"  # Matches 'Z', '+02:00', '+0200', '+02', etc.
 FULL_RE = rf"{DATE_RE}{SEP_RE}?{TIME_RE}?{TZ_RE}?$"
 
@@ -354,6 +356,10 @@ DATE_FORMATS = (
     (DMY_RE, "%d-%m-%Y"),
     (MDY_RE, "%m-%d-%Y"),
 )
+TIME_FORMATS = (
+    (HMS_RE, "%H:%M:%S"),
+    (HM_RE, "%H:%M"),
+)
 
 
 def parse_datetime_format(arr: pa.StringArray) -> str:
@@ -418,5 +424,8 @@ def _parse_date_format(arr: pa.Array) -> str:
 def _parse_time_format(arr: pa.Array) -> str:
     import pyarrow.compute as pc  # ignore-banned-import
 
-    matches = pc.extract_regex(arr, pattern=TIME_RE)
-    return "%H:%M:%S" if pc.all(matches.is_valid()).as_py() else ""
+    for time_rgx, time_fmt in TIME_FORMATS:
+        matches = pc.extract_regex(arr, pattern=time_rgx)
+        if pc.all(matches.is_valid()).as_py():
+            return time_fmt
+    return ""


Test patch:
diff --git a/tests/expr_and_series/str/to_datetime_test.py b/tests/expr_and_series/str/to_datetime_test.py
index 0d93b706..9682bbb1 100644
--- a/tests/expr_and_series/str/to_datetime_test.py
+++ b/tests/expr_and_series/str/to_datetime_test.py
@@ -47,11 +47,29 @@ def test_to_datetime_series(constructor_eager: ConstructorEager) -> None:
     assert str(result) == expected
 
 
-def test_to_datetime_infer_fmt(constructor: Constructor) -> None:
+@pytest.mark.parametrize(
+    ("data", "expected", "expected_cudf"),
+    [
+        (
+            {"a": ["2020-01-01T12:34:56"]},
+            "2020-01-01 12:34:56",
+            "2020-01-01T12:34:56.000000000",
+        ),
+        (
+            {"a": ["2020-01-01T12:34"]},
+            "2020-01-01 12:34:00",
+            "2020-01-01T12:34:00.000000000",
+        ),
+    ],
+)
+def test_to_datetime_infer_fmt(
+    constructor: Constructor,
+    data: dict[str, list[str]],
+    expected: str,
+    expected_cudf: str,
+) -> None:
     if "cudf" in str(constructor):  # pragma: no cover
-        expected = "2020-01-01T12:34:56.000000000"
-    else:
-        expected = "2020-01-01 12:34:56"
+        expected = expected_cudf
 
     result = (
         nw.from_native(constructor(data))
@@ -63,11 +81,29 @@ def test_to_datetime_infer_fmt(constructor: Constructor) -> None:
     assert str(result) == expected
 
 
-def test_to_datetime_series_infer_fmt(constructor_eager: ConstructorEager) -> None:
+@pytest.mark.parametrize(
+    ("data", "expected", "expected_cudf"),
+    [
+        (
+            {"a": ["2020-01-01T12:34:56"]},
+            "2020-01-01 12:34:56",
+            "2020-01-01T12:34:56.000000000",
+        ),
+        (
+            {"a": ["2020-01-01T12:34"]},
+            "2020-01-01 12:34:00",
+            "2020-01-01T12:34:00.000000000",
+        ),
+    ],
+)
+def test_to_datetime_series_infer_fmt(
+    constructor_eager: ConstructorEager,
+    data: dict[str, list[str]],
+    expected: str,
+    expected_cudf: str,
+) -> None:
     if "cudf" in str(constructor_eager):  # pragma: no cover
-        expected = "2020-01-01T12:34:56.000000000"
-    else:
-        expected = "2020-01-01 12:34:56"
+        expected = expected_cudf
 
     result = (
         nw.from_native(constructor_eager(data), eager_only=True)["a"].str.to_datetime()