Repository: narwhals-dev/narwhals
Pull request #225 (2024-05-29 09:56:32)

Template for PR 
https://github.com/narwhals-dev/narwhals/issues/218

Issue template on another PR

Feedback welcome

<img width="1042" alt="Screenshot 2024-05-29 at 11 55 43" src="https://github.com/narwhals-dev/narwhals/assets/11835246/9332770e-3a62-45b8-8480-ab436995d489">


MarcoGorelli: thanks! @InessaPawson fancy taking a look?
DeaMariaLeon: Thanks for the feedback @FBruzzesi! I'll wait a bit for Inessa's feedback, then I'll update everything together. 
If that's OK. :)  
DeaMariaLeon: Thank you all for the feedback. If there is anything else, please tell me. 

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..15b685ac
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,29 @@
+
+<!--
+# 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
+- [ ] ✨ Feature
+- [ ] 🐛 Bug Fix
+- [ ] 🔧 Optimization
+- [ ] 📝 Documentation
+- [ ] ✅ Test
+- [ ] 🐳 Other
+
+## Related issues 
+
+- Related issue # 
+- Closes #
+
+## Checklist
+
+- [ ] Code follows style guide (ruff)
+- [ ] Tests added 
+- [ ] Documented the changes
+
+## If you have comments or can explain your changes, please do so below.
+
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..e97c987f 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.
@@ -2321,15 +2373,89 @@ def len() -> Expr:
 
 def sum(*columns: str) -> Expr:
     """
-    Instantiate an expression representing the sum of one or more columns, similar to `polars.sum`.
+    Sum all values.
+
+    Note:
+        Syntactic sugar for ``nw.col(columns).sum()``
+
+    Parameters:
+        *columns
+            Name(s) of the columns to use in the aggregation function
+
+    Example:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import narwhals as nw
+        >>> df_pl = pl.DataFrame({"a": [1, 2]})
+        >>> df_pd = pd.DataFrame({"a": [1, 2]})
+
+    We define a dataframe-agnostic function:
+
+        >>> def func(df_any):
+        ...     df = nw.from_native(df_any)
+        ...     df = df.select(nw.sum('a'))
+        ...     return nw.to_native(df)
+
+    We can then pass either pandas or polars to `func`:
+
+        >>> func(df_pd)
+           a
+        0  3
+        >>> func(df_pl)
+        shape: (1, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ i64 │
+        ╞═════╡
+        │ 3   │
+        └─────┘
     """
+
     return Expr(lambda plx: plx.sum(*columns))
 
 
 def mean(*columns: str) -> Expr:
     """
-    Instantiate an expression representing the mean of one or more columns, similar to `polars.mean`.
+    Get the mean value.
+
+    Note:
+        Syntactic sugar for ``nw.col(columns).mean()``
+
+    Parameters:
+        *columns
+            Name(s) of the columns to use in the aggregation function
+
+    Example:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import narwhals as nw
+        >>> df_pl = pl.DataFrame({"a": [1, 8, 3]})
+        >>> df_pd = pd.DataFrame({"a": [1, 8, 3]})
+
+    We define a dataframe agnostic function:
+
+        >>> def func(df_any):
+        ...     df = nw.from_native(df_any)
+        ...     df = df.select(nw.mean('a'))
+        ...     return nw.to_native(df)
+
+    We can then pass either pandas or Polars to `func`:
+
+        >>> func(df_pd)
+             a
+        0  4.0
+        >>> func(df_pl)
+        shape: (1, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ f64 │
+        ╞═════╡
+        │ 4.0 │
+        └─────┘
     """
+
     return Expr(lambda plx: plx.mean(*columns))
 
 
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]),
     ],
 )