Skip to content

Commit 2b3a45f

Browse files
authored
feat: add .dt.days, .dt.seconds, dt.microseconds, and dt.total_seconds() for timedelta series. (#1713)
* feat: add part accessors to timedelta series * fix doctest
1 parent 08d70b6 commit 2b3a45f

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

bigframes/operations/datetimes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@
1919

2020
import bigframes_vendored.pandas.core.arrays.datetimelike as vendored_pandas_datetimelike
2121
import bigframes_vendored.pandas.core.indexes.accessor as vendordt
22+
import pandas
2223

24+
from bigframes import dtypes
2325
from bigframes.core import log_adapter
2426
import bigframes.operations as ops
2527
import bigframes.operations.base
2628
import bigframes.series as series
2729

30+
_ONE_DAY = pandas.Timedelta("1d")
31+
_ONE_SECOND = pandas.Timedelta("1s")
32+
_ONE_MICRO = pandas.Timedelta("1us")
33+
2834

2935
@log_adapter.class_logger
3036
class DatetimeMethods(
@@ -80,6 +86,35 @@ def second(self) -> series.Series:
8086
def time(self) -> series.Series:
8187
return self._apply_unary_op(ops.time_op)
8288

89+
# Timedelta accessors
90+
@property
91+
def days(self) -> series.Series:
92+
self._check_dtype(dtypes.TIMEDELTA_DTYPE)
93+
94+
return self._apply_binary_op(_ONE_DAY, ops.floordiv_op)
95+
96+
@property
97+
def seconds(self) -> series.Series:
98+
self._check_dtype(dtypes.TIMEDELTA_DTYPE)
99+
100+
return self._apply_binary_op(_ONE_DAY, ops.mod_op) // _ONE_SECOND # type: ignore
101+
102+
@property
103+
def microseconds(self) -> series.Series:
104+
self._check_dtype(dtypes.TIMEDELTA_DTYPE)
105+
106+
return self._apply_binary_op(_ONE_SECOND, ops.mod_op) // _ONE_MICRO # type: ignore
107+
108+
def total_seconds(self) -> series.Series:
109+
self._check_dtype(dtypes.TIMEDELTA_DTYPE)
110+
111+
return self._apply_binary_op(_ONE_SECOND, ops.div_op)
112+
113+
def _check_dtype(self, target_dtype: dtypes.Dtype):
114+
if self._dtype == target_dtype:
115+
return
116+
raise TypeError(f"Expect dtype: {target_dtype}, but got {self._dtype}")
117+
83118
@property
84119
def tz(self) -> Optional[dt.timezone]:
85120
# Assumption: pyarrow dtype

tests/system/small/operations/test_datetimes.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@
3030
]
3131

3232

33+
@pytest.fixture
34+
def timedelta_series(session):
35+
pd_s = pd.Series(pd.to_timedelta([1.1010101, 2.2020102, 3.3030103], unit="d"))
36+
bf_s = session.read_pandas(pd_s)
37+
38+
return bf_s, pd_s
39+
40+
3341
@pytest.mark.parametrize(
3442
("col_name",),
3543
DATE_COLUMNS,
@@ -489,3 +497,39 @@ def test_timestamp_series_diff_agg(scalars_dfs, column):
489497

490498
expected_result = pd_series.diff()
491499
assert_series_equal(actual_result, expected_result)
500+
501+
502+
@pytest.mark.parametrize(
503+
"access",
504+
[
505+
pytest.param(lambda x: x.dt.days, id="dt.days"),
506+
pytest.param(lambda x: x.dt.seconds, id="dt.seconds"),
507+
pytest.param(lambda x: x.dt.microseconds, id="dt.microseconds"),
508+
pytest.param(lambda x: x.dt.total_seconds(), id="dt.total_seconds()"),
509+
],
510+
)
511+
def test_timedelta_dt_accessors(timedelta_series, access):
512+
bf_s, pd_s = timedelta_series
513+
514+
actual_result = access(bf_s).to_pandas()
515+
516+
expected_result = access(pd_s)
517+
assert_series_equal(
518+
actual_result, expected_result, check_dtype=False, check_index_type=False
519+
)
520+
521+
522+
@pytest.mark.parametrize(
523+
"access",
524+
[
525+
pytest.param(lambda x: x.dt.days, id="dt.days"),
526+
pytest.param(lambda x: x.dt.seconds, id="dt.seconds"),
527+
pytest.param(lambda x: x.dt.microseconds, id="dt.microseconds"),
528+
pytest.param(lambda x: x.dt.total_seconds(), id="dt.total_seconds()"),
529+
],
530+
)
531+
def test_timedelta_dt_accessors_on_wrong_type_raise_exception(scalars_dfs, access):
532+
bf_df, _ = scalars_dfs
533+
534+
with pytest.raises(TypeError):
535+
access(bf_df["timestamp_col"])

third_party/bigframes_vendored/pandas/core/indexes/accessor.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,77 @@ def year(self):
299299

300300
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
301301

302+
@property
303+
def days(self):
304+
"""The numebr of days for each element
305+
306+
**Examples:**
307+
308+
>>> import pandas as pd
309+
>>> import bigframes.pandas as bpd
310+
>>> bpd.options.display.progress_bar = None
311+
>>> s = bpd.Series([pd.Timedelta("4d3m2s1us")])
312+
>>> s
313+
0 4 days 00:03:02.000001
314+
dtype: duration[us][pyarrow]
315+
>>> s.dt.days
316+
0 4
317+
dtype: Int64
318+
"""
319+
320+
@property
321+
def seconds(self):
322+
"""Number of seconds (>= 0 and less than 1 day) for each element.
323+
324+
**Examples:**
325+
326+
>>> import pandas as pd
327+
>>> import bigframes.pandas as bpd
328+
>>> bpd.options.display.progress_bar = None
329+
>>> s = bpd.Series([pd.Timedelta("4d3m2s1us")])
330+
>>> s
331+
0 4 days 00:03:02.000001
332+
dtype: duration[us][pyarrow]
333+
>>> s.dt.seconds
334+
0 182
335+
dtype: Int64
336+
"""
337+
338+
@property
339+
def microseconds(self):
340+
"""Number of microseconds (>= 0 and less than 1 second) for each element.
341+
342+
**Examples:**
343+
344+
>>> import pandas as pd
345+
>>> import bigframes.pandas as bpd
346+
>>> bpd.options.display.progress_bar = None
347+
>>> s = bpd.Series([pd.Timedelta("4d3m2s1us")])
348+
>>> s
349+
0 4 days 00:03:02.000001
350+
dtype: duration[us][pyarrow]
351+
>>> s.dt.microseconds
352+
0 1
353+
dtype: Int64
354+
"""
355+
356+
def total_seconds(self):
357+
"""Return total duration of each element expressed in seconds.
358+
359+
**Examples:**
360+
361+
>>> import pandas as pd
362+
>>> import bigframes.pandas as bpd
363+
>>> bpd.options.display.progress_bar = None
364+
>>> s = bpd.Series([pd.Timedelta("1d1m1s1us")])
365+
>>> s
366+
0 1 days 00:01:01.000001
367+
dtype: duration[us][pyarrow]
368+
>>> s.dt.total_seconds()
369+
0 86461.000001
370+
dtype: Float64
371+
"""
372+
302373
@property
303374
def tz(self):
304375
"""Return the timezone.

0 commit comments

Comments
 (0)