Skip to content

Commit 3901951

Browse files
chore: define timedelta type and to_timedelta function (#1317)
* define timedelta type and to_timedelta function * remove unnecessary file * remove TypeAlias type for 3.9 compatibility * fix mypy * fix lint * move timedelta out of the simple dtype list * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * fix type casts in tests --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent fe4fbb4 commit 3901951

File tree

12 files changed

+321
-0
lines changed

12 files changed

+321
-0
lines changed

bigframes/core/compile/ibis_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
BIGFRAMES_TO_IBIS: Dict[bigframes.dtypes.Dtype, ibis_dtypes.DataType] = {
8282
pandas: ibis for ibis, pandas in BIDIRECTIONAL_MAPPINGS
8383
}
84+
BIGFRAMES_TO_IBIS.update({bigframes.dtypes.TIMEDETLA_DTYPE: ibis_dtypes.int64})
8485
IBIS_TO_BIGFRAMES: Dict[ibis_dtypes.DataType, bigframes.dtypes.Dtype] = {
8586
ibis: pandas for ibis, pandas in BIDIRECTIONAL_MAPPINGS
8687
}

bigframes/core/compile/scalar_op_compiler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,13 @@ def to_timestamp_op_impl(x: ibis_types.Value, op: ops.ToTimestampOp):
11401140
return x.cast(ibis_dtypes.Timestamp(timezone="UTC"))
11411141

11421142

1143+
@scalar_op_compiler.register_unary_op(ops.ToTimedeltaOp, pass_op=True)
1144+
def to_timedelta_op_impl(x: ibis_types.Value, op: ops.ToTimedeltaOp):
1145+
return (
1146+
typing.cast(ibis_types.NumericValue, x) * UNIT_TO_US_CONVERSION_FACTORS[op.unit] # type: ignore
1147+
).floor()
1148+
1149+
11431150
@scalar_op_compiler.register_unary_op(ops.RemoteFunctionOp, pass_op=True)
11441151
def remote_function_op_impl(x: ibis_types.Value, op: ops.RemoteFunctionOp):
11451152
ibis_node = getattr(op.func, "ibis_node", None)

bigframes/dtypes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
TIME_DTYPE = pd.ArrowDtype(pa.time64("us"))
5656
DATETIME_DTYPE = pd.ArrowDtype(pa.timestamp("us"))
5757
TIMESTAMP_DTYPE = pd.ArrowDtype(pa.timestamp("us", tz="UTC"))
58+
TIMEDETLA_DTYPE = pd.ArrowDtype(pa.duration("us"))
5859
NUMERIC_DTYPE = pd.ArrowDtype(pa.decimal128(38, 9))
5960
BIGNUMERIC_DTYPE = pd.ArrowDtype(pa.decimal256(76, 38))
6061
# No arrow equivalent
@@ -632,6 +633,9 @@ def convert_to_schema_field(
632633
return google.cloud.bigquery.SchemaField(
633634
name, "RECORD", fields=inner_fields
634635
)
636+
if bigframes_dtype.pyarrow_dtype == pa.duration("us"):
637+
# Timedeltas are represented as integers in microseconds.
638+
return google.cloud.bigquery.SchemaField(name, "INTEGER")
635639
raise ValueError(
636640
f"No arrow conversion for {bigframes_dtype}. {constants.FEEDBACK_LINK}"
637641
)

bigframes/operations/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
)
171171
from bigframes.operations.struct_ops import StructFieldOp, StructOp
172172
from bigframes.operations.time_ops import hour_op, minute_op, normalize_op, second_op
173+
from bigframes.operations.timedelta_ops import ToTimedeltaOp
173174

174175
__all__ = [
175176
# Base ops
@@ -240,6 +241,8 @@
240241
"minute_op",
241242
"second_op",
242243
"normalize_op",
244+
# Timedelta ops
245+
"ToTimedeltaOp",
243246
# Datetime ops
244247
"date_op",
245248
"time_op",

bigframes/operations/timedelta_ops.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import dataclasses
17+
import typing
18+
19+
from bigframes import dtypes
20+
from bigframes.operations import base_ops
21+
22+
23+
@dataclasses.dataclass(frozen=True)
24+
class ToTimedeltaOp(base_ops.UnaryOp):
25+
name: typing.ClassVar[str] = "to_timedelta"
26+
unit: typing.Literal["us", "ms", "s", "m", "h", "d", "W"]
27+
28+
def output_type(self, *input_types):
29+
if input_types[0] is not dtypes.INT_DTYPE:
30+
raise TypeError("expected integer input")
31+
return dtypes.TIMEDETLA_DTYPE

bigframes/pandas/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import bigframes.dataframe
3636
import bigframes.enums
3737
import bigframes.functions._utils as bff_utils
38+
from bigframes.pandas.core.api import to_timedelta
3839
from bigframes.pandas.io.api import (
3940
from_glob_path,
4041
read_csv,
@@ -313,6 +314,7 @@ def reset_session():
313314
"read_pickle",
314315
"remote_function",
315316
"to_datetime",
317+
"to_timedelta",
316318
"from_glob_path",
317319
# pandas dtype attributes
318320
"NA",

bigframes/pandas/core/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.

bigframes/pandas/core/api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from bigframes.pandas.core.tools.timedeltas import to_timedelta
16+
17+
__all__ = ["to_timedelta"]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import typing
16+
17+
from bigframes_vendored.pandas.core.tools import (
18+
timedeltas as vendored_pandas_timedeltas,
19+
)
20+
import pandas as pd
21+
22+
from bigframes import operations as ops
23+
from bigframes import series
24+
25+
26+
def to_timedelta(
27+
arg: typing.Union[series.Series, str, int, float],
28+
unit: typing.Optional[vendored_pandas_timedeltas.UnitChoices] = None,
29+
) -> typing.Union[series.Series, pd.Timedelta]:
30+
if not isinstance(arg, series.Series):
31+
return pd.to_timedelta(arg, unit)
32+
33+
canonical_unit = "us" if unit is None else _canonicalize_unit(unit)
34+
return arg._apply_unary_op(ops.ToTimedeltaOp(canonical_unit))
35+
36+
37+
to_timedelta.__doc__ = vendored_pandas_timedeltas.to_timedelta.__doc__
38+
39+
40+
def _canonicalize_unit(
41+
unit: vendored_pandas_timedeltas.UnitChoices,
42+
) -> typing.Literal["us", "ms", "s", "m", "h", "d", "W"]:
43+
if unit in {"w", "W"}:
44+
return "W"
45+
46+
if unit in {"D", "d", "days", "day"}:
47+
return "d"
48+
49+
if unit in {"hours", "hour", "hr", "h"}:
50+
return "h"
51+
52+
if unit in {"m", "minute", "min", "minutes"}:
53+
return "m"
54+
55+
if unit in {"s", "seconds", "sec", "second"}:
56+
return "s"
57+
58+
if unit in {"ms", "milliseconds", "millisecond", "milli", "millis"}:
59+
return "ms"
60+
61+
if unit in {"us", "microseconds", "microsecond", "µs", "micro", "micros"}:
62+
return "us"
63+
64+
raise TypeError(f"Unrecognized unit: {unit}")

0 commit comments

Comments
 (0)