Skip to content

Commit 50ad3a5

Browse files
chore: support timestamp_sub (googleapis#1390)
* [WIP] support timestamp_sub * add timestamp_sub tests * fix format * fix format * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix error message --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 26f351a commit 50ad3a5

File tree

6 files changed

+127
-1
lines changed

6 files changed

+127
-1
lines changed

bigframes/core/compile/scalar_op_compiler.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,11 @@ def timestamp_add_op_impl(x: ibis_types.TimestampValue, y: ibis_types.IntegerVal
747747
return x + y.to_interval("us")
748748

749749

750+
@scalar_op_compiler.register_binary_op(ops.timestamp_sub_op)
751+
def timestamp_sub_op_impl(x: ibis_types.TimestampValue, y: ibis_types.IntegerValue):
752+
return x - y.to_interval("us")
753+
754+
750755
@scalar_op_compiler.register_unary_op(ops.FloorDtOp, pass_op=True)
751756
def floor_dt_op_impl(x: ibis_types.Value, op: ops.FloorDtOp):
752757
supported_freqs = ["Y", "Q", "M", "W", "D", "h", "min", "s", "ms", "us", "ns"]

bigframes/core/rewrite/timedeltas.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ def _rewrite_sub_op(left: _TypedExpr, right: _TypedExpr) -> _TypedExpr:
110110
if dtypes.is_datetime_like(left.dtype) and dtypes.is_datetime_like(right.dtype):
111111
return _TypedExpr.create_op_expr(ops.timestamp_diff_op, left, right)
112112

113+
if dtypes.is_datetime_like(left.dtype) and right.dtype is dtypes.TIMEDELTA_DTYPE:
114+
return _TypedExpr.create_op_expr(ops.timestamp_sub_op, left, right)
115+
113116
return _TypedExpr.create_op_expr(ops.sub_op, left, right)
114117

115118

bigframes/operations/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,11 @@
178178
)
179179
from bigframes.operations.struct_ops import StructFieldOp, StructOp
180180
from bigframes.operations.time_ops import hour_op, minute_op, normalize_op, second_op
181-
from bigframes.operations.timedelta_ops import timestamp_add_op, ToTimedeltaOp
181+
from bigframes.operations.timedelta_ops import (
182+
timestamp_add_op,
183+
timestamp_sub_op,
184+
ToTimedeltaOp,
185+
)
182186

183187
__all__ = [
184188
# Base ops
@@ -251,6 +255,7 @@
251255
"normalize_op",
252256
# Timedelta ops
253257
"timestamp_add_op",
258+
"timestamp_sub_op",
254259
"ToTimedeltaOp",
255260
# Datetime ops
256261
"date_op",

bigframes/operations/numeric_ops.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ def output_type(self, *input_types):
151151
if dtypes.is_datetime_like(left_type) and dtypes.is_datetime_like(right_type):
152152
return dtypes.TIMEDELTA_DTYPE
153153

154+
if dtypes.is_datetime_like(left_type) and right_type is dtypes.TIMEDELTA_DTYPE:
155+
return left_type
156+
154157
raise TypeError(f"Cannot subtract dtypes {left_type} and {right_type}")
155158

156159

bigframes/operations/timedelta_ops.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,23 @@ def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionT
5454

5555

5656
timestamp_add_op = TimestampAdd()
57+
58+
59+
@dataclasses.dataclass(frozen=True)
60+
class TimestampSub(base_ops.BinaryOp):
61+
name: typing.ClassVar[str] = "timestamp_sub"
62+
63+
def output_type(self, *input_types: dtypes.ExpressionType) -> dtypes.ExpressionType:
64+
# timestamp - timedelta => timestamp
65+
if (
66+
dtypes.is_datetime_like(input_types[0])
67+
and input_types[1] is dtypes.TIMEDELTA_DTYPE
68+
):
69+
return input_types[0]
70+
71+
raise TypeError(
72+
f"unsupported types for timestamp_sub. left: {input_types[0]} right: {input_types[1]}"
73+
)
74+
75+
76+
timestamp_sub_op = TimestampSub()

tests/system/small/operations/test_timedeltas.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,96 @@ def test_timestamp_add_dataframes(temporal_dfs):
178178
)
179179

180180

181+
@pytest.mark.parametrize(
182+
("column", "pd_dtype"),
183+
[
184+
("datetime_col", "<M8[ns]"),
185+
("timestamp_col", "datetime64[ns, UTC]"),
186+
],
187+
)
188+
def test_timestamp_sub__ts_series_minus_td_series(temporal_dfs, column, pd_dtype):
189+
bf_df, pd_df = temporal_dfs
190+
191+
actual_result = (
192+
(bf_df[column] - bf_df["timedelta_col_1"]).to_pandas().astype(pd_dtype)
193+
)
194+
195+
expected_result = pd_df[column] - pd_df["timedelta_col_1"]
196+
pandas.testing.assert_series_equal(
197+
actual_result, expected_result, check_index_type=False
198+
)
199+
200+
201+
@pytest.mark.parametrize(
202+
("column", "pd_dtype"),
203+
[
204+
("datetime_col", "<M8[ns]"),
205+
("timestamp_col", "datetime64[ns, UTC]"),
206+
],
207+
)
208+
def test_timestamp_sub__ts_series_minus_td_literal(temporal_dfs, column, pd_dtype):
209+
bf_df, pd_df = temporal_dfs
210+
literal = pd.Timedelta(1, "h")
211+
212+
actual_result = (bf_df[column] - literal).to_pandas().astype(pd_dtype)
213+
214+
expected_result = pd_df[column] - literal
215+
pandas.testing.assert_series_equal(
216+
actual_result, expected_result, check_index_type=False
217+
)
218+
219+
220+
def test_timestamp_sub__ts_literal_minus_td_series(temporal_dfs):
221+
bf_df, pd_df = temporal_dfs
222+
literal = pd.Timestamp("2025-01-01 01:00:00")
223+
224+
actual_result = (literal - bf_df["timedelta_col_1"]).to_pandas().astype("<M8[ns]")
225+
226+
expected_result = literal - pd_df["timedelta_col_1"]
227+
pandas.testing.assert_series_equal(
228+
actual_result, expected_result, check_index_type=False
229+
)
230+
231+
232+
@pytest.mark.parametrize(
233+
("column", "pd_dtype"),
234+
[
235+
("datetime_col", "<M8[ns]"),
236+
("timestamp_col", "datetime64[ns, UTC]"),
237+
],
238+
)
239+
def test_timestamp_sub_with_numpy_op(temporal_dfs, column, pd_dtype):
240+
bf_df, pd_df = temporal_dfs
241+
242+
actual_result = (
243+
np.subtract(bf_df[column], bf_df["timedelta_col_1"])
244+
.to_pandas()
245+
.astype(pd_dtype)
246+
)
247+
248+
expected_result = np.subtract(pd_df[column], pd_df["timedelta_col_1"])
249+
pandas.testing.assert_series_equal(
250+
actual_result, expected_result, check_index_type=False
251+
)
252+
253+
254+
def test_timestamp_sub_dataframes(temporal_dfs):
255+
columns = ["datetime_col", "timestamp_col"]
256+
timedelta = pd.Timedelta(1, unit="s")
257+
bf_df, pd_df = temporal_dfs
258+
259+
actual_result = (bf_df[columns] - timedelta).to_pandas()
260+
actual_result["datetime_col"] = actual_result["datetime_col"].astype("<M8[ns]")
261+
actual_result["timestamp_col"] = actual_result["timestamp_col"].astype(
262+
"datetime64[ns, UTC]"
263+
)
264+
265+
expected_result = pd_df[columns] - timedelta
266+
pandas.testing.assert_frame_equal(
267+
actual_result, expected_result, check_index_type=False
268+
)
269+
270+
181271
@pytest.mark.parametrize(
182272
"compare_func",
183273
[

0 commit comments

Comments
 (0)