Repository: narwhals-dev/narwhals
Pull request #135 (2024-05-10 19:29:54)

Added millisecond and microsecond temporal methods


Patch:
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index f9020824..c9e021a8 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -328,6 +328,12 @@ class PandasExprDateTimeNamespace:
     def second(self) -> PandasExpr:
         return register_namespace_expression_call(self._expr, "dt", "second")
 
+    def millisecond(self) -> PandasExpr:
+        return register_namespace_expression_call(self._expr, "dt", "millisecond")
+
+    def microsecond(self) -> PandasExpr:
+        return register_namespace_expression_call(self._expr, "dt", "microsecond")
+
     def ordinal_day(self) -> PandasExpr:
         return register_namespace_expression_call(self._expr, "dt", "ordinal_day")
 
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 819492bf..b52dc37c 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -546,6 +546,14 @@ class PandasSeriesDateTimeNamespace:
             self._series._series.dt.second,
         )
 
+    def millisecond(self) -> PandasSeries:
+        return self._series._from_series(
+            self._series._series.dt.microsecond // 1000,
+        )
+
+    def microsecond(self) -> PandasSeries:
+        return self._series._from_series(self._series._series.dt.microsecond)
+
     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 fbddc1e3..b8c73e00 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -1769,6 +1769,110 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.second())
 
+    def millisecond(self) -> Expr:
+        """
+        Extract milliseconds 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, 505000),
+            ...         datetime(2065, 1, 1, 10, 20, 30, 67000),
+            ...     ]
+            ... }
+            >>> 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.millisecond().alias("millisecond")
+            ...     )
+            ...     return nw.to_native(df)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+                             datetime  hour  minute  second  millisecond
+            0 1978-01-01 01:01:01.000     1       1       1            0
+            1 2024-10-13 05:30:14.505     5      30      14          505
+            2 2065-01-01 10:20:30.067    10      20      30           67
+            >>> func(df_pl)
+            shape: (3, 5)
+            ┌─────────────────────────┬──────┬────────┬────────┬─────────────┐
+            │ datetime                ┆ hour ┆ minute ┆ second ┆ millisecond │
+            │ ---                     ┆ ---  ┆ ---    ┆ ---    ┆ ---         │
+            │ datetime[μs]            ┆ i8   ┆ i8     ┆ i8     ┆ i32         │
+            ╞═════════════════════════╪══════╪════════╪════════╪═════════════╡
+            │ 1978-01-01 01:01:01     ┆ 1    ┆ 1      ┆ 1      ┆ 0           │
+            │ 2024-10-13 05:30:14.505 ┆ 5    ┆ 30     ┆ 14     ┆ 505         │
+            │ 2065-01-01 10:20:30.067 ┆ 10   ┆ 20     ┆ 30     ┆ 67          │
+            └─────────────────────────┴──────┴────────┴────────┴─────────────┘
+        """
+        return self._expr.__class__(lambda plx: self._expr._call(plx).dt.millisecond())
+
+    def microsecond(self) -> Expr:
+        """
+        Extract microseconds 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, 505000),
+            ...         datetime(2065, 1, 1, 10, 20, 30, 67000),
+            ...     ]
+            ... }
+            >>> 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.microsecond().alias("microsecond")
+            ...     )
+            ...     return nw.to_native(df)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+                             datetime  hour  minute  second  microsecond
+            0 1978-01-01 01:01:01.000     1       1       1            0
+            1 2024-10-13 05:30:14.505     5      30      14       505000
+            2 2065-01-01 10:20:30.067    10      20      30        67000
+            >>> func(df_pl)
+            shape: (3, 5)
+            ┌─────────────────────────┬──────┬────────┬────────┬─────────────┐
+            │ datetime                ┆ hour ┆ minute ┆ second ┆ microsecond │
+            │ ---                     ┆ ---  ┆ ---    ┆ ---    ┆ ---         │
+            │ datetime[μs]            ┆ i8   ┆ i8     ┆ i8     ┆ i32         │
+            ╞═════════════════════════╪══════╪════════╪════════╪═════════════╡
+            │ 1978-01-01 01:01:01     ┆ 1    ┆ 1      ┆ 1      ┆ 0           │
+            │ 2024-10-13 05:30:14.505 ┆ 5    ┆ 30     ┆ 14     ┆ 505000      │
+            │ 2065-01-01 10:20:30.067 ┆ 10   ┆ 20     ┆ 30     ┆ 67000       │
+            └─────────────────────────┴──────┴────────┴────────┴─────────────┘
+        """
+        return self._expr.__class__(lambda plx: self._expr._call(plx).dt.microsecond())
+
     def ordinal_day(self) -> Expr:
         """
         Get ordinal day.
diff --git a/narwhals/series.py b/narwhals/series.py
index adc9ac51..4827b34f 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -1455,16 +1455,16 @@ class SeriesDateTimeNamespace:
 
     def year(self) -> Series:
         """
-        Get the year in a date series.
+        Get the year in a datetime series.
 
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
             >>> from datetime import datetime
             >>> import narwhals as nw
-            >>> data = [datetime(2012, 1, 7), datetime(2023, 3, 10)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
+            >>> dates = [datetime(2012, 1, 7), datetime(2023, 3, 10)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
 
             We define a library agnostic function:
 
@@ -1491,16 +1491,16 @@ class SeriesDateTimeNamespace:
 
     def month(self) -> Series:
         """
-        Gets the month in a date series.
+        Gets the month in a datetime series.
 
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
             >>> from datetime import datetime
             >>> import narwhals as nw
-            >>> data = [datetime(2023, 2, 1), datetime(2023, 8, 3)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
+            >>> dates = [datetime(2023, 2, 1), datetime(2023, 8, 3)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
 
             We define a library agnostic function:
 
@@ -1527,16 +1527,16 @@ class SeriesDateTimeNamespace:
 
     def day(self) -> Series:
         """
-        Extracts the day in a date series.
+        Extracts the day in a datetime series.
 
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
             >>> from datetime import datetime
             >>> import narwhals as nw
-            >>> data = [datetime(2022, 1, 1), datetime(2022, 1, 5)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
+            >>> dates = [datetime(2022, 1, 1), datetime(2022, 1, 5)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
 
             We define a library agnostic function:
 
@@ -1563,16 +1563,16 @@ class SeriesDateTimeNamespace:
 
     def hour(self) -> Series:
         """
-         Extracts the hour in a date series.
+         Extracts the hour in a datetime series.
 
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
             >>> from datetime import datetime
             >>> import narwhals as nw
-            >>> data = [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
+            >>> dates = [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
 
             We define a library agnostic function:
 
@@ -1599,16 +1599,16 @@ class SeriesDateTimeNamespace:
 
     def minute(self) -> Series:
         """
-        Extracts the minute in a date series.
+        Extracts the minute in a datetime series.
 
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
             >>> from datetime import datetime
             >>> import narwhals as nw
-            >>> data = [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
+            >>> dates = [datetime(2022, 1, 1, 5, 3), datetime(2022, 1, 5, 9, 12)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
 
             We define a library agnostic function:
 
@@ -1635,16 +1635,16 @@ class SeriesDateTimeNamespace:
 
     def second(self) -> Series:
         """
-        Extracts the second(s) in a date series.
+        Extracts the second(s) in a datetime series.
 
         Examples:
             >>> import pandas as pd
             >>> import polars as pl
             >>> from datetime import datetime
             >>> import narwhals as nw
-            >>> data = [datetime(2022, 1, 1, 5, 3, 10), datetime(2022, 1, 5, 9, 12, 4)]
-            >>> s_pd = pd.Series(data)
-            >>> s_pl = pl.Series(data)
+            >>> dates = [datetime(2022, 1, 1, 5, 3, 10), datetime(2022, 1, 5, 9, 12, 4)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
 
             We define a library agnostic function:
 
@@ -1669,6 +1669,104 @@ class SeriesDateTimeNamespace:
         """
         return self._series.__class__(self._series._series.dt.second())
 
+    def millisecond(self) -> Series:
+        """
+        Extracts the milliseconds in a datetime series.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> from datetime import datetime
+            >>> import narwhals as nw
+            >>> dates = [
+            ...     datetime(2023, 5, 21, 12, 55, 10, 400000),
+            ...     datetime(2023, 5, 21, 12, 55, 10, 600000),
+            ...     datetime(2023, 5, 21, 12, 55, 10, 800000),
+            ...     datetime(2023, 5, 21, 12, 55, 11, 0),
+            ...     datetime(2023, 5, 21, 12, 55, 11, 200000)
+            ... ]
+
+            >>> 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.millisecond().alias("datetime")
+            ...     return nw.to_native(s)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    400
+            1    600
+            2    800
+            3      0
+            4    200
+            Name: datetime, dtype: int...
+            >>> func(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (5,)
+            Series: 'datetime' [i32]
+            [
+                400
+                600
+                800
+                0
+                200
+            ]
+        """
+        return self._series.__class__(self._series._series.dt.millisecond())
+
+    def microsecond(self) -> Series:
+        """
+        Extracts the microseconds in a datetime series.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> from datetime import datetime
+            >>> import narwhals as nw
+            >>> dates = [
+            ...     datetime(2023, 5, 21, 12, 55, 10, 400000),
+            ...     datetime(2023, 5, 21, 12, 55, 10, 600000),
+            ...     datetime(2023, 5, 21, 12, 55, 10, 800000),
+            ...     datetime(2023, 5, 21, 12, 55, 11, 0),
+            ...     datetime(2023, 5, 21, 12, 55, 11, 200000)
+            ... ]
+
+            >>> 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.microsecond().alias("datetime")
+            ...     return nw.to_native(s)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    400000
+            1    600000
+            2    800000
+            3         0
+            4    200000
+            Name: datetime, dtype: int...
+            >>> func(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (5,)
+            Series: 'datetime' [i32]
+            [
+               400000
+               600000
+               800000
+               0
+               200000
+            ]
+        """
+        return self._series.__class__(self._series._series.dt.microsecond())
+
     def ordinal_day(self) -> Series:
         """
         Get ordinal day.


Test patch:
diff --git a/tests/test_dt.py b/tests/test_dt.py
index 5e727606..03ef8721 100644
--- a/tests/test_dt.py
+++ b/tests/test_dt.py
@@ -17,8 +17,8 @@ from tests.utils import compare_dicts
 
 data = {
     "a": [
-        datetime(2021, 3, 1, 12, 34, 56),
-        datetime(2020, 1, 2, 2, 4, 14),
+        datetime(2021, 3, 1, 12, 34, 56, 49000),
+        datetime(2020, 1, 2, 2, 4, 14, 715000),
     ],
 }
 data_timedelta = {
@@ -44,6 +44,8 @@ data_timedelta = {
         ("hour", [12, 2]),
         ("minute", [34, 4]),
         ("second", [56, 14]),
+        ("millisecond", [49, 715]),
+        ("microsecond", [49000, 715000]),
         ("ordinal_day", [60, 2]),
     ],
 )