Skip to content

Commit 2e1a403

Browse files
feat: add Series dt.tz and dt.unit properties (#303)
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/python-bigquery-dataframes/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #<issue_number_goes_here> 🦕
1 parent f645c56 commit 2e1a403

File tree

3 files changed

+107
-44
lines changed

3 files changed

+107
-44
lines changed

bigframes/operations/datetimes.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
from __future__ import annotations
1616

17+
import datetime as dt
18+
from typing import Optional
19+
1720
from bigframes.core import log_adapter
1821
import bigframes.operations as ops
1922
import bigframes.operations.base
@@ -27,6 +30,7 @@ class DatetimeMethods(
2730
):
2831
__doc__ = vendordt.DatetimeProperties.__doc__
2932

33+
# Date accessors
3034
@property
3135
def day(self) -> series.Series:
3236
return self._apply_unary_op(ops.day_op)
@@ -40,17 +44,26 @@ def date(self) -> series.Series:
4044
return self._apply_unary_op(ops.date_op)
4145

4246
@property
43-
def hour(self) -> series.Series:
44-
return self._apply_unary_op(ops.hour_op)
47+
def quarter(self) -> series.Series:
48+
return self._apply_unary_op(ops.quarter_op)
4549

4650
@property
47-
def minute(self) -> series.Series:
48-
return self._apply_unary_op(ops.minute_op)
51+
def year(self) -> series.Series:
52+
return self._apply_unary_op(ops.year_op)
4953

5054
@property
5155
def month(self) -> series.Series:
5256
return self._apply_unary_op(ops.month_op)
5357

58+
# Time accessors
59+
@property
60+
def hour(self) -> series.Series:
61+
return self._apply_unary_op(ops.hour_op)
62+
63+
@property
64+
def minute(self) -> series.Series:
65+
return self._apply_unary_op(ops.minute_op)
66+
5467
@property
5568
def second(self) -> series.Series:
5669
return self._apply_unary_op(ops.second_op)
@@ -60,9 +73,17 @@ def time(self) -> series.Series:
6073
return self._apply_unary_op(ops.time_op)
6174

6275
@property
63-
def quarter(self) -> series.Series:
64-
return self._apply_unary_op(ops.quarter_op)
76+
def tz(self) -> Optional[dt.timezone]:
77+
# Assumption: pyarrow dtype
78+
tz_string = self._dtype.pyarrow_dtype.tz
79+
if tz_string == "UTC":
80+
return dt.timezone.utc
81+
elif tz_string is None:
82+
return None
83+
else:
84+
raise ValueError(f"Unexpected timezone {tz_string}")
6585

6686
@property
67-
def year(self) -> series.Series:
68-
return self._apply_unary_op(ops.year_op)
87+
def unit(self) -> str:
88+
# Assumption: pyarrow dtype
89+
return self._dtype.pyarrow_dtype.unit

tests/system/small/operations/test_datetimes.py

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@
1616
import pytest
1717

1818
import bigframes.series
19-
from tests.system.utils import assert_series_equal
19+
from tests.system.utils import assert_series_equal, skip_legacy_pandas
2020

2121
DATETIME_COL_NAMES = [("datetime_col",), ("timestamp_col",)]
22+
DATE_COLUMNS = [
23+
("datetime_col",),
24+
("timestamp_col",),
25+
("date_col",),
26+
]
2227

2328

2429
@pytest.mark.parametrize(
2530
("col_name",),
26-
DATETIME_COL_NAMES,
31+
DATE_COLUMNS,
2732
)
28-
def test_day(scalars_dfs, col_name):
29-
if pd.__version__.startswith("1."):
30-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
33+
@skip_legacy_pandas
34+
def test_dt_day(scalars_dfs, col_name):
3135
scalars_df, scalars_pandas_df = scalars_dfs
3236
bf_series: bigframes.series.Series = scalars_df[col_name]
3337
bf_result = bf_series.dt.day.to_pandas()
@@ -43,9 +47,8 @@ def test_day(scalars_dfs, col_name):
4347
("col_name",),
4448
DATETIME_COL_NAMES,
4549
)
46-
def test_date(scalars_dfs, col_name):
47-
if pd.__version__.startswith("1."):
48-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
50+
@skip_legacy_pandas
51+
def test_dt_date(scalars_dfs, col_name):
4952
scalars_df, scalars_pandas_df = scalars_dfs
5053
bf_series: bigframes.series.Series = scalars_df[col_name]
5154
bf_result = bf_series.dt.date.to_pandas()
@@ -59,11 +62,10 @@ def test_date(scalars_dfs, col_name):
5962

6063
@pytest.mark.parametrize(
6164
("col_name",),
62-
DATETIME_COL_NAMES,
65+
DATE_COLUMNS,
6366
)
64-
def test_dayofweek(scalars_dfs, col_name):
65-
if pd.__version__.startswith("1."):
66-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
67+
@skip_legacy_pandas
68+
def test_dt_dayofweek(scalars_dfs, col_name):
6769
scalars_df, scalars_pandas_df = scalars_dfs
6870
bf_series: bigframes.series.Series = scalars_df[col_name]
6971
bf_result = bf_series.dt.dayofweek.to_pandas()
@@ -76,9 +78,8 @@ def test_dayofweek(scalars_dfs, col_name):
7678
("col_name",),
7779
DATETIME_COL_NAMES,
7880
)
79-
def test_hour(scalars_dfs, col_name):
80-
if pd.__version__.startswith("1."):
81-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
81+
@skip_legacy_pandas
82+
def test_dt_hour(scalars_dfs, col_name):
8283
scalars_df, scalars_pandas_df = scalars_dfs
8384
bf_series: bigframes.series.Series = scalars_df[col_name]
8485
bf_result = bf_series.dt.hour.to_pandas()
@@ -94,9 +95,8 @@ def test_hour(scalars_dfs, col_name):
9495
("col_name",),
9596
DATETIME_COL_NAMES,
9697
)
97-
def test_minute(scalars_dfs, col_name):
98-
if pd.__version__.startswith("1."):
99-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
98+
@skip_legacy_pandas
99+
def test_dt_minute(scalars_dfs, col_name):
100100
scalars_df, scalars_pandas_df = scalars_dfs
101101
bf_series: bigframes.series.Series = scalars_df[col_name]
102102
bf_result = bf_series.dt.minute.to_pandas()
@@ -110,11 +110,10 @@ def test_minute(scalars_dfs, col_name):
110110

111111
@pytest.mark.parametrize(
112112
("col_name",),
113-
DATETIME_COL_NAMES,
113+
DATE_COLUMNS,
114114
)
115-
def test_month(scalars_dfs, col_name):
116-
if pd.__version__.startswith("1."):
117-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
115+
@skip_legacy_pandas
116+
def test_dt_month(scalars_dfs, col_name):
118117
scalars_df, scalars_pandas_df = scalars_dfs
119118
bf_series: bigframes.series.Series = scalars_df[col_name]
120119
bf_result = bf_series.dt.month.to_pandas()
@@ -128,11 +127,10 @@ def test_month(scalars_dfs, col_name):
128127

129128
@pytest.mark.parametrize(
130129
("col_name",),
131-
DATETIME_COL_NAMES,
130+
DATE_COLUMNS,
132131
)
133-
def test_quarter(scalars_dfs, col_name):
134-
if pd.__version__.startswith("1."):
135-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
132+
@skip_legacy_pandas
133+
def test_dt_quarter(scalars_dfs, col_name):
136134
scalars_df, scalars_pandas_df = scalars_dfs
137135
bf_series: bigframes.series.Series = scalars_df[col_name]
138136
bf_result = bf_series.dt.quarter.to_pandas()
@@ -148,9 +146,8 @@ def test_quarter(scalars_dfs, col_name):
148146
("col_name",),
149147
DATETIME_COL_NAMES,
150148
)
151-
def test_second(scalars_dfs, col_name):
152-
if pd.__version__.startswith("1."):
153-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
149+
@skip_legacy_pandas
150+
def test_dt_second(scalars_dfs, col_name):
154151
scalars_df, scalars_pandas_df = scalars_dfs
155152
bf_series: bigframes.series.Series = scalars_df[col_name]
156153
bf_result = bf_series.dt.second.to_pandas()
@@ -166,9 +163,8 @@ def test_second(scalars_dfs, col_name):
166163
("col_name",),
167164
DATETIME_COL_NAMES,
168165
)
169-
def test_time(scalars_dfs, col_name):
170-
if pd.__version__.startswith("1."):
171-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
166+
@skip_legacy_pandas
167+
def test_dt_time(scalars_dfs, col_name):
172168
scalars_df, scalars_pandas_df = scalars_dfs
173169
bf_series: bigframes.series.Series = scalars_df[col_name]
174170
bf_result = bf_series.dt.time.to_pandas()
@@ -182,11 +178,10 @@ def test_time(scalars_dfs, col_name):
182178

183179
@pytest.mark.parametrize(
184180
("col_name",),
185-
DATETIME_COL_NAMES,
181+
DATE_COLUMNS,
186182
)
187-
def test_year(scalars_dfs, col_name):
188-
if pd.__version__.startswith("1."):
189-
pytest.skip("Pyarrow datetime objects not support in pandas 1.x.")
183+
@skip_legacy_pandas
184+
def test_dt_year(scalars_dfs, col_name):
190185
scalars_df, scalars_pandas_df = scalars_dfs
191186
bf_series: bigframes.series.Series = scalars_df[col_name]
192187
bf_result = bf_series.dt.year.to_pandas()
@@ -196,3 +191,31 @@ def test_year(scalars_dfs, col_name):
196191
pd_result.astype(pd.Int64Dtype()),
197192
bf_result,
198193
)
194+
195+
196+
@pytest.mark.parametrize(
197+
("col_name",),
198+
DATETIME_COL_NAMES,
199+
)
200+
@skip_legacy_pandas
201+
def test_dt_tz(scalars_dfs, col_name):
202+
scalars_df, scalars_pandas_df = scalars_dfs
203+
bf_series: bigframes.series.Series = scalars_df[col_name]
204+
bf_result = bf_series.dt.tz
205+
pd_result = scalars_pandas_df[col_name].dt.tz
206+
207+
assert bf_result == pd_result
208+
209+
210+
@pytest.mark.parametrize(
211+
("col_name",),
212+
DATETIME_COL_NAMES,
213+
)
214+
@skip_legacy_pandas
215+
def test_dt_unit(scalars_dfs, col_name):
216+
scalars_df, scalars_pandas_df = scalars_dfs
217+
bf_series: bigframes.series.Series = scalars_df[col_name]
218+
bf_result = bf_series.dt.unit
219+
pd_result = scalars_pandas_df[col_name].dt.unit
220+
221+
assert bf_result == pd_result

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,22 @@ def year(self):
9494
"""The year of the datetime."""
9595

9696
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
97+
98+
@property
99+
def tz(self):
100+
"""Return the timezone.
101+
102+
Returns:
103+
datetime.tzinfo, pytz.tzinfo.BaseTZInfo, dateutil.tz.tz.tzfile, or None
104+
"""
105+
106+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)
107+
108+
@property
109+
def unit(self) -> str:
110+
"""Returns the unit of time precision.
111+
112+
Returns:
113+
Unit as string (eg. "us").
114+
"""
115+
raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)

0 commit comments

Comments
 (0)