Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 99389a1

Browse files
committed
feat: add dt.tz_localize()
1 parent 0f1ac1a commit 99389a1

File tree

5 files changed

+90
-5
lines changed

5 files changed

+90
-5
lines changed

bigframes/core/compile/ibis_compiler/scalar_op_registry.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import functools
1818
import typing
19-
from typing import cast
19+
from typing import cast, Union
2020

2121
from bigframes_vendored import ibis
2222
import bigframes_vendored.ibis.expr.api as ibis_api
@@ -978,7 +978,7 @@ def isin_op_impl(x: ibis_types.Value, op: ops.IsInOp):
978978

979979
@scalar_op_compiler.register_unary_op(ops.ToDatetimeOp, pass_op=True)
980980
def to_datetime_op_impl(x: ibis_types.Value, op: ops.ToDatetimeOp):
981-
if x.type() == ibis_dtypes.str:
981+
if x.type() in (ibis_dtypes.str, ibis_dtypes.Timestamp("UTC")):
982982
return x.try_cast(ibis_dtypes.Timestamp(None)) # type: ignore
983983
else:
984984
# Numerical inputs.
@@ -1001,6 +1001,8 @@ def to_timestamp_op_impl(x: ibis_types.Value, op: ops.ToTimestampOp):
10011001
if op.format
10021002
else timestamp(x)
10031003
)
1004+
elif x.type() == ibis_dtypes.Timestamp(None): # Datetime type
1005+
return timestamp(x)
10041006
else:
10051007
# Numerical inputs.
10061008
if op.format:
@@ -2016,8 +2018,8 @@ def _ibis_num(number: float):
20162018

20172019

20182020
@ibis_udf.scalar.builtin
2019-
def timestamp(a: str) -> ibis_dtypes.timestamp: # type: ignore
2020-
"""Convert string to timestamp."""
2021+
def timestamp(a) -> ibis_dtypes.timestamp: # type: ignore
2022+
"""Convert string or a datetime to timestamp."""
20212023

20222024

20232025
@ibis_udf.scalar.builtin

bigframes/operations/datetime_ops.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT
7373
dtypes.INT_DTYPE,
7474
dtypes.STRING_DTYPE,
7575
dtypes.DATE_DTYPE,
76+
dtypes.TIMESTAMP_DTYPE,
7677
):
7778
raise TypeError("expected string or numeric input")
7879
return pd.ArrowDtype(pa.timestamp("us", tz=None))
@@ -91,6 +92,7 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT
9192
dtypes.INT_DTYPE,
9293
dtypes.STRING_DTYPE,
9394
dtypes.DATE_DTYPE,
95+
dtypes.DATETIME_DTYPE,
9496
):
9597
raise TypeError("expected string or numeric input")
9698
return pd.ArrowDtype(pa.timestamp("us", tz="UTC"))

bigframes/operations/datetimes.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from __future__ import annotations
1616

1717
import datetime as dt
18-
from typing import Optional
18+
from typing import Literal, Optional
1919

2020
import bigframes_vendored.pandas.core.arrays.datetimelike as vendored_pandas_datetimelike
2121
import bigframes_vendored.pandas.core.indexes.accessor as vendordt
@@ -147,6 +147,21 @@ def tz(self) -> Optional[dt.timezone]:
147147
else:
148148
raise ValueError(f"Unexpected timezone {tz_string}")
149149

150+
def tz_localize(self, tz: Literal["UTC"] | None) -> series.Series:
151+
if tz == "UTC":
152+
if self._data.dtype == dtypes.TIMESTAMP_DTYPE:
153+
raise ValueError("Already tz-aware.")
154+
155+
return self._data._apply_unary_op(ops.ToTimestampOp())
156+
157+
if tz is None:
158+
if self._data.dtype == dtypes.DATETIME_DTYPE:
159+
return self._data # no-op
160+
161+
return self._data._apply_unary_op(ops.ToDatetimeOp())
162+
163+
raise ValueError(f"Unsupported timezone {tz}")
164+
150165
@property
151166
def unit(self) -> str:
152167
# Assumption: pyarrow dtype

tests/system/small/operations/test_datetimes.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,42 @@ def test_dt_tz(scalars_dfs, col_name):
324324
assert bf_result == pd_result
325325

326326

327+
@pytest.mark.parametrize(
328+
("col_name", "tz"),
329+
[
330+
("datetime_col", None),
331+
("timestamp_col", None),
332+
("datetime_col", "UTC"),
333+
],
334+
)
335+
def test_dt_tz_localize(scalars_dfs, col_name, tz):
336+
pytest.importorskip("pandas", minversion="2.0.0")
337+
scalars_df, scalars_pandas_df = scalars_dfs
338+
bf_series = scalars_df[col_name]
339+
340+
bf_result = bf_series.dt.tz_localize(tz)
341+
pd_result = scalars_pandas_df[col_name].dt.tz_localize(tz)
342+
343+
testing.assert_series_equal(
344+
bf_result.to_pandas(), pd_result, check_index_type=False
345+
)
346+
347+
348+
@pytest.mark.parametrize(
349+
("col_name", "tz"),
350+
[
351+
("timestamp_col", "UTC"),
352+
("datetime_col", "US/Eastern"),
353+
],
354+
)
355+
def test_dt_tz_localize_invalid_inputs(scalars_dfs, col_name, tz):
356+
pytest.importorskip("pandas", minversion="2.0.0")
357+
scalars_df, _ = scalars_dfs
358+
359+
with pytest.raises(ValueError):
360+
scalars_df[col_name].dt.tz_localize(tz)
361+
362+
327363
@pytest.mark.parametrize(
328364
("col_name",),
329365
DATETIME_COL_NAMES,

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Literal
2+
13
from bigframes import constants
24

35

@@ -499,6 +501,34 @@ def tz(self):
499501

500502
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
501503

504+
@property
505+
def tz_localize(self, tz: Literal["UTC"] | None):
506+
"""Localize tz-naive Datetime Array/Index to tz-aware Datetime Array/Index.
507+
508+
This method takes a time zone (tz) naive Datetime Array/Index object and makes
509+
this time zone aware. It does not move the time to another time zone. Only "UTC"
510+
timezone is supported.
511+
512+
This method can also be used to do the inverse - to create a time zone unaware
513+
object from an aware object. To that end, pass tz=None.
514+
515+
**Examples:**
516+
517+
>>> import bigframes.pandas as bpd
518+
>>> s = bpd.Series([pd.Timestamp(year = 2026, month=1, day=1)])
519+
>>> s
520+
0 2026-01-01 00:00:00
521+
dtype: timestamp[us][pyarrow]
522+
>>> s.dt.tz_localize('UTC')
523+
0 2026-01-01 00:00:00+00:00
524+
dtype: timestamp[us, tz=UTC][pyarrow]
525+
526+
Returns:
527+
A BigFrames series with the updated timezone.
528+
"""
529+
530+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
531+
502532
@property
503533
def unit(self) -> str:
504534
"""Returns the unit of time precision.

0 commit comments

Comments
 (0)