Skip to content

Commit 8f6e955

Browse files
feat: support Series.dt.strftime (#453)
* feat: support Series.dt.strftime * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * address comments * fix imports * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 1df0140 commit 8f6e955

File tree

5 files changed

+110
-1
lines changed

5 files changed

+110
-1
lines changed

bigframes/core/compile/scalar_op_compiler.py

+9
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,15 @@ def second_op_impl(x: ibis_types.Value):
613613
return typing.cast(ibis_types.TimestampValue, x).second().cast(ibis_dtypes.int64)
614614

615615

616+
@scalar_op_compiler.register_unary_op(ops.StrftimeOp, pass_op=True)
617+
def strftime_op_impl(x: ibis_types.Value, op: ops.StrftimeOp):
618+
return (
619+
typing.cast(ibis_types.TimestampValue, x)
620+
.strftime(op.date_format)
621+
.cast(ibis_dtypes.str)
622+
)
623+
624+
616625
@scalar_op_compiler.register_unary_op(ops.time_op)
617626
def time_op_impl(x: ibis_types.Value):
618627
return typing.cast(ibis_types.TimestampValue, x).time()

bigframes/operations/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ def output_type(self, *input_types):
415415
return input_types[0]
416416

417417

418+
@dataclasses.dataclass(frozen=True)
419+
class StrftimeOp(UnaryOp):
420+
name: typing.ClassVar[str] = "strftime"
421+
date_format: str
422+
423+
def output_type(self, *input_types):
424+
return dtypes.STRING_DTYPE
425+
426+
418427
# Binary Ops
419428
fillna_op = create_binary_op(name="fillna")
420429
cliplower_op = create_binary_op(name="clip_lower")

bigframes/operations/datetimes.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import datetime as dt
1818
from typing import Optional
1919

20+
import bigframes_vendored.pandas.core.arrays.datetimelike as vendored_pandas_datetimelike
2021
import bigframes_vendored.pandas.core.indexes.accessor as vendordt
2122

2223
from bigframes.core import log_adapter
@@ -27,7 +28,9 @@
2728

2829
@log_adapter.class_logger
2930
class DatetimeMethods(
30-
bigframes.operations.base.SeriesMethods, vendordt.DatetimeProperties
31+
bigframes.operations.base.SeriesMethods,
32+
vendordt.DatetimeProperties,
33+
vendored_pandas_datetimelike.DatelikeOps,
3134
):
3235
__doc__ = vendordt.DatetimeProperties.__doc__
3336

@@ -88,3 +91,6 @@ def tz(self) -> Optional[dt.timezone]:
8891
def unit(self) -> str:
8992
# Assumption: pyarrow dtype
9093
return self._dtype.pyarrow_dtype.unit
94+
95+
def strftime(self, date_format: str) -> series.Series:
96+
return self._apply_unary_op(ops.StrftimeOp(date_format=date_format))

tests/system/small/operations/test_datetimes.py

+47
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,50 @@ def test_dt_unit(scalars_dfs, col_name):
219219
pd_result = scalars_pandas_df[col_name].dt.unit
220220

221221
assert bf_result == pd_result
222+
223+
224+
@pytest.mark.parametrize(
225+
("column", "date_format"),
226+
[
227+
("timestamp_col", "%B %d, %Y, %r"),
228+
("timestamp_col", "%m-%d-%Y %H:%M"),
229+
("datetime_col", "%m-%d-%Y %H:%M"),
230+
("datetime_col", "%H:%M"),
231+
],
232+
)
233+
@skip_legacy_pandas
234+
def test_dt_strftime(scalars_df_index, scalars_pandas_df_index, column, date_format):
235+
bf_result = scalars_df_index[column].dt.strftime(date_format).to_pandas()
236+
pd_result = scalars_pandas_df_index[column].dt.strftime(date_format)
237+
pd.testing.assert_series_equal(bf_result, pd_result, check_dtype=False)
238+
assert bf_result.dtype == "string[pyarrow]"
239+
240+
241+
def test_dt_strftime_date():
242+
bf_series = bigframes.series.Series(
243+
["2014-08-15", "2215-08-15", "2016-02-29"]
244+
).astype("date32[day][pyarrow]")
245+
246+
expected_result = pd.Series(["08/15/2014", "08/15/2215", "02/29/2016"])
247+
bf_result = bf_series.dt.strftime("%m/%d/%Y").to_pandas()
248+
249+
pd.testing.assert_series_equal(
250+
bf_result, expected_result, check_index_type=False, check_dtype=False
251+
)
252+
assert bf_result.dtype == "string[pyarrow]"
253+
254+
255+
def test_dt_strftime_time():
256+
bf_series = bigframes.series.Series(
257+
[143542314, 345234512341, 75543252344, 626546437654754, 8543523452345234]
258+
).astype("time64[us][pyarrow]")
259+
260+
expected_result = pd.Series(
261+
["00:02:23", "23:53:54", "20:59:03", "16:40:37", "08:57:32"]
262+
)
263+
bf_result = bf_series.dt.strftime("%X").to_pandas()
264+
265+
pd.testing.assert_series_equal(
266+
bf_result, expected_result, check_index_type=False, check_dtype=False
267+
)
268+
assert bf_result.dtype == "string[pyarrow]"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Contains code from https://github.com/pandas-dev/pandas/blob/main/pandas/core/arrays/datetimelike.py
2+
3+
from bigframes import constants
4+
5+
6+
class DatelikeOps:
7+
def strftime(self, date_format: str):
8+
"""
9+
Convert to string Series using specified date_format.
10+
11+
Return a Series of formatted strings specified by date_format. Details
12+
of the string format can be found in `BigQuery format elements doc
13+
<%(https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements)s>`__.
14+
15+
**Examples:**
16+
17+
>>> import bigframes.pandas as bpd
18+
>>> bpd.options.display.progress_bar = None
19+
20+
>>> s = bpd.to_datetime(
21+
... ['2014-08-15 08:15:12', '2012-02-29 08:15:12+06:00', '2015-08-15 08:15:12+05:00'],
22+
... utc=True
23+
... ).astype("timestamp[us, tz=UTC][pyarrow]")
24+
25+
>>> s.dt.strftime("%B %d, %Y, %r")
26+
0 August 15, 2014, 08:15:12 AM
27+
1 February 29, 2012, 02:15:12 AM
28+
2 August 15, 2015, 03:15:12 AM
29+
Name: 0, dtype: string
30+
31+
Args:
32+
date_format (str):
33+
Date format string (e.g. "%Y-%m-%d").
34+
35+
Returns:
36+
bigframes.series.Series of formatted strings.
37+
"""
38+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)

0 commit comments

Comments
 (0)