Repository: narwhals-dev/narwhals
Pull request #217 (2024-05-27 15:18:38)

nanoseconds
I accidentally force pushed so I need to open a new one. 

MarcoGorelli: > I accidentally force pushed so I need to open a new one.

force pushing is fine, you could've kept the old one 😉 but this is fine too
MarcoGorelli: thanks for updating! there's some merge conflicts, could you resolve them please?

Patch:
diff --git a/docs/api-reference/expressions_dt.md b/docs/api-reference/expressions_dt.md
index fc42f2e5..bdd47191 100644
--- a/docs/api-reference/expressions_dt.md
+++ b/docs/api-reference/expressions_dt.md
@@ -13,6 +13,7 @@
         - second
         - millisecond
         - microsecond
+        - nanosecond
         - total_minutes
         - total_seconds
         - total_milliseconds
diff --git a/docs/api-reference/series_dt.md b/docs/api-reference/series_dt.md
index af555f65..f3b274d2 100644
--- a/docs/api-reference/series_dt.md
+++ b/docs/api-reference/series_dt.md
@@ -13,6 +13,7 @@
         - second
         - millisecond
         - microsecond
+        - nanosecond
         - total_minutes
         - total_seconds
         - total_milliseconds
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 53ab6910..afe57e78 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -354,6 +354,9 @@ class PandasExprDateTimeNamespace:
     def microsecond(self) -> PandasExpr:
         return reuse_series_namespace_implementation(self._expr, "dt", "microsecond")
 
+    def nanosecond(self) -> PandasExpr:
+        return reuse_series_namespace_implementation(self._expr, "dt", "nanosecond")
+
     def ordinal_day(self) -> PandasExpr:
         return reuse_series_namespace_implementation(self._expr, "dt", "ordinal_day")
 
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 96d6004c..eebc7f3f 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -557,6 +557,14 @@ class PandasSeriesDateTimeNamespace:
     def microsecond(self) -> PandasSeries:
         return self._series._from_series(self._series._series.dt.microsecond)
 
+    def nanosecond(self) -> PandasSeries:
+        return self._series._from_series(
+            (
+                (self._series._series.dt.microsecond * 1_000)
+                + self._series._series.dt.nanosecond
+            ),
+        )
+
     def ordinal_day(self) -> PandasSeries:
         ser = self._series._series
         year_start = ser.dt.year
diff --git a/narwhals/expression.py b/narwhals/expression.py
index fbb292a4..2f0f0e95 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -2007,6 +2007,58 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.microsecond())
 
+    def nanosecond(self) -> Expr:
+        """
+        Extract Nanoseconds from underlying DateTime representation
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> from datetime import datetime
+            >>> import narwhals as nw
+            >>> data = {
+            ...     "datetime": [
+            ...         datetime(1978, 1, 1, 1, 1, 1, 0),
+            ...         datetime(2024, 10, 13, 5, 30, 14, 500000),
+            ...         datetime(2065, 1, 1, 10, 20, 30, 60000)
+            ...     ]
+            ... }
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+
+            We define a dataframe-agnostic function:
+
+            >>> def func(df_any):
+            ...     df = nw.from_native(df_any)
+            ...     df = df.with_columns(
+            ...         nw.col("datetime").dt.hour().alias("hour"),
+            ...         nw.col("datetime").dt.minute().alias("minute"),
+            ...         nw.col("datetime").dt.second().alias("second"),
+            ...         nw.col("datetime").dt.nanosecond().alias("nanosecond")
+            ...     )
+            ...     return nw.to_native(df)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+                             datetime  hour  minute  second  nanosecond
+            0 1978-01-01 01:01:01.000     1       1       1           0
+            1 2024-10-13 05:30:14.500     5      30      14   500000000
+            2 2065-01-01 10:20:30.060    10      20      30    60000000
+            >>> func(df_pl)
+            shape: (3, 5)
+            ┌─────────────────────────┬──────┬────────┬────────┬────────────┐
+            │ datetime                ┆ hour ┆ minute ┆ second ┆ nanosecond │
+            │ ---                     ┆ ---  ┆ ---    ┆ ---    ┆ ---        │
+            │ datetime[μs]            ┆ i8   ┆ i8     ┆ i8     ┆ i32        │
+            ╞═════════════════════════╪══════╪════════╪════════╪════════════╡
+            │ 1978-01-01 01:01:01     ┆ 1    ┆ 1      ┆ 1      ┆ 0          │
+            │ 2024-10-13 05:30:14.500 ┆ 5    ┆ 30     ┆ 14     ┆ 500000000  │
+            │ 2065-01-01 10:20:30.060 ┆ 10   ┆ 20     ┆ 30     ┆ 60000000   │
+            └─────────────────────────┴──────┴────────┴────────┴────────────┘
+        """
+        return self._expr.__class__(lambda plx: self._expr._call(plx).dt.nanosecond())
+
     def ordinal_day(self) -> Expr:
         """
         Get ordinal day.
diff --git a/narwhals/series.py b/narwhals/series.py
index 466f05a9..a987660a 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -1955,6 +1955,42 @@ class SeriesDateTimeNamespace:
         """
         return self._series.__class__(self._series._series.dt.microsecond())
 
+    def nanosecond(self) -> Series:
+        """
+        Extracts the nanosecond(s) in a date series.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> from datetime import datetime
+            >>> import narwhals as nw
+            >>> dates = [datetime(2022, 1, 1, 5, 3, 10, 500000), datetime(2022, 1, 5, 9, 12, 4, 60000)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
+
+            We define a library agnostic function:
+
+            >>> def func(s_any):
+            ...     s = nw.from_native(s_any, series_only=True)
+            ...     s = s.dt.nanosecond()
+            ...     return nw.to_native(s)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    500000000
+            1     60000000
+            dtype: int...
+            >>> func(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (2,)
+            Series: '' [i32]
+            [
+               500000000
+               60000000
+            ]
+        """
+        return self._series.__class__(self._series._series.dt.nanosecond())
+
     def ordinal_day(self) -> Series:
         """
         Get ordinal day.


Test patch:
diff --git a/tests/test_dt.py b/tests/test_dt.py
index 03ef8721..97a8d97c 100644
--- a/tests/test_dt.py
+++ b/tests/test_dt.py
@@ -46,6 +46,7 @@ data_timedelta = {
         ("second", [56, 14]),
         ("millisecond", [49, 715]),
         ("microsecond", [49000, 715000]),
+        ("nanosecond", [49000000, 715000000]),
         ("ordinal_day", [60, 2]),
     ],
 )