Skip to content

Commit e4b0c8d

Browse files
authored
feat: add unix_seconds, unix_millis and unix_micros for timestamp series. (#1297)
* feat: add UNIX_SECONDS, UNIX_MILLIS and UNIX_MICROS functions for timestamp series * add docstring * fix format * fix mypy error * fix type error
1 parent ee37a0a commit e4b0c8d

File tree

6 files changed

+227
-0
lines changed

6 files changed

+227
-0
lines changed

bigframes/bigquery/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
array_length,
2323
array_to_string,
2424
)
25+
from bigframes.bigquery._operations.datetime import (
26+
unix_micros,
27+
unix_millis,
28+
unix_seconds,
29+
)
2530
from bigframes.bigquery._operations.json import (
2631
json_extract,
2732
json_extract_array,
@@ -53,4 +58,8 @@
5358
"sql_scalar",
5459
# struct ops
5560
"struct",
61+
# datetime ops
62+
"unix_micros",
63+
"unix_millis",
64+
"unix_seconds",
5665
]
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 import operations as ops
16+
from bigframes import series
17+
18+
19+
def unix_seconds(input: series.Series) -> series.Series:
20+
"""Converts a timestmap series to unix epoch seconds
21+
22+
**Examples:**
23+
24+
>>> import pandas as pd
25+
>>> import bigframes.pandas as bpd
26+
>>> import bigframes.bigquery as bbq
27+
>>> bpd.options.display.progress_bar = None
28+
29+
>>> s = bpd.Series([pd.Timestamp("1970-01-02", tz="UTC"), pd.Timestamp("1970-01-03", tz="UTC")])
30+
>>> bbq.unix_seconds(s)
31+
0 86400
32+
1 172800
33+
dtype: Int64
34+
35+
Args:
36+
input (bigframes.pandas.Series):
37+
A timestamp series.
38+
39+
Returns:
40+
bigframes.pandas.Series: A new series of unix epoch in seconds.
41+
42+
"""
43+
return input._apply_unary_op(ops.UnixSeconds())
44+
45+
46+
def unix_millis(input: series.Series) -> series.Series:
47+
"""Converts a timestmap series to unix epoch milliseconds
48+
49+
**Examples:**
50+
51+
>>> import pandas as pd
52+
>>> import bigframes.pandas as bpd
53+
>>> import bigframes.bigquery as bbq
54+
>>> bpd.options.display.progress_bar = None
55+
56+
>>> s = bpd.Series([pd.Timestamp("1970-01-02", tz="UTC"), pd.Timestamp("1970-01-03", tz="UTC")])
57+
>>> bbq.unix_millis(s)
58+
0 86400000
59+
1 172800000
60+
dtype: Int64
61+
62+
Args:
63+
input (bigframes.pandas.Series):
64+
A timestamp series.
65+
66+
Returns:
67+
bigframes.pandas.Series: A new series of unix epoch in milliseconds.
68+
69+
"""
70+
return input._apply_unary_op(ops.UnixMillis())
71+
72+
73+
def unix_micros(input: series.Series) -> series.Series:
74+
"""Converts a timestmap series to unix epoch microseconds
75+
76+
**Examples:**
77+
78+
>>> import pandas as pd
79+
>>> import bigframes.pandas as bpd
80+
>>> import bigframes.bigquery as bbq
81+
>>> bpd.options.display.progress_bar = None
82+
83+
>>> s = bpd.Series([pd.Timestamp("1970-01-02", tz="UTC"), pd.Timestamp("1970-01-03", tz="UTC")])
84+
>>> bbq.unix_micros(s)
85+
0 86400000000
86+
1 172800000000
87+
dtype: Int64
88+
89+
Args:
90+
input (bigframes.pandas.Series):
91+
A timestamp series.
92+
93+
Returns:
94+
bigframes.pandas.Series: A new series of unix epoch in microseconds.
95+
96+
"""
97+
return input._apply_unary_op(ops.UnixMicros())

bigframes/core/compile/scalar_op_compiler.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,21 @@ def strftime_op_impl(x: ibis_types.Value, op: ops.StrftimeOp):
722722
)
723723

724724

725+
@scalar_op_compiler.register_unary_op(ops.UnixSeconds)
726+
def unix_seconds_op_impl(x: ibis_types.TimestampValue):
727+
return x.epoch_seconds()
728+
729+
730+
@scalar_op_compiler.register_unary_op(ops.UnixMicros)
731+
def unix_micros_op_impl(x: ibis_types.TimestampValue):
732+
return unix_micros(x)
733+
734+
735+
@scalar_op_compiler.register_unary_op(ops.UnixMillis)
736+
def unix_millis_op_impl(x: ibis_types.TimestampValue):
737+
return unix_millis(x)
738+
739+
725740
@scalar_op_compiler.register_unary_op(ops.FloorDtOp, pass_op=True)
726741
def floor_dt_op_impl(x: ibis_types.Value, op: ops.FloorDtOp):
727742
supported_freqs = ["Y", "Q", "M", "W", "D", "h", "min", "s", "ms", "us", "ns"]
@@ -1887,6 +1902,16 @@ def timestamp(a: str) -> ibis_dtypes.timestamp: # type: ignore
18871902
"""Convert string to timestamp."""
18881903

18891904

1905+
@ibis_udf.scalar.builtin
1906+
def unix_millis(a: ibis_dtypes.timestamp) -> int: # type: ignore
1907+
"""Convert a timestamp to milliseconds"""
1908+
1909+
1910+
@ibis_udf.scalar.builtin
1911+
def unix_micros(a: ibis_dtypes.timestamp) -> int: # type: ignore
1912+
"""Convert a timestamp to microseconds"""
1913+
1914+
18901915
# Need these because ibis otherwise tries to do casts to int that can fail
18911916
@ibis_udf.scalar.builtin(name="floor")
18921917
def float_floor(a: float) -> float:

bigframes/operations/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
time_op,
5252
ToDatetimeOp,
5353
ToTimestampOp,
54+
UnixMicros,
55+
UnixMillis,
56+
UnixSeconds,
5457
)
5558
from bigframes.operations.distance_ops import (
5659
cosine_distance_op,
@@ -243,6 +246,9 @@
243246
"ToDatetimeOp",
244247
"ToTimestampOp",
245248
"StrftimeOp",
249+
"UnixMicros",
250+
"UnixMillis",
251+
"UnixSeconds",
246252
# Numeric ops
247253
"abs_op",
248254
"add_op",

bigframes/operations/datetime_ops.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,27 @@ class StrftimeOp(base_ops.UnaryOp):
7777

7878
def output_type(self, *input_types):
7979
return dtypes.STRING_DTYPE
80+
81+
82+
@dataclasses.dataclass(frozen=True)
83+
class UnixSeconds(base_ops.UnaryOp):
84+
name: typing.ClassVar[str] = "unix_seconds"
85+
86+
def output_type(self, *input_types):
87+
return dtypes.INT_DTYPE
88+
89+
90+
@dataclasses.dataclass(frozen=True)
91+
class UnixMillis(base_ops.UnaryOp):
92+
name: typing.ClassVar[str] = "unix_millis"
93+
94+
def output_type(self, *input_types):
95+
return dtypes.INT_DTYPE
96+
97+
98+
@dataclasses.dataclass(frozen=True)
99+
class UnixMicros(base_ops.UnaryOp):
100+
name: typing.ClassVar[str] = "unix_micros"
101+
102+
def output_type(self, *input_types):
103+
return dtypes.INT_DTYPE
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
import pandas as pd
18+
19+
from bigframes import bigquery
20+
21+
22+
def test_unix_seconds(scalars_dfs):
23+
bigframes_df, pandas_df = scalars_dfs
24+
25+
actual_res = bigquery.unix_seconds(bigframes_df["timestamp_col"]).to_pandas()
26+
27+
expected_res = (
28+
pandas_df["timestamp_col"]
29+
.apply(lambda ts: _to_unix_epoch(ts, "s"))
30+
.astype("Int64")
31+
)
32+
pd.testing.assert_series_equal(actual_res, expected_res)
33+
34+
35+
def test_unix_millis(scalars_dfs):
36+
bigframes_df, pandas_df = scalars_dfs
37+
38+
actual_res = bigquery.unix_millis(bigframes_df["timestamp_col"]).to_pandas()
39+
40+
expected_res = (
41+
pandas_df["timestamp_col"]
42+
.apply(lambda ts: _to_unix_epoch(ts, "ms"))
43+
.astype("Int64")
44+
)
45+
pd.testing.assert_series_equal(actual_res, expected_res)
46+
47+
48+
def test_unix_micros(scalars_dfs):
49+
bigframes_df, pandas_df = scalars_dfs
50+
51+
actual_res = bigquery.unix_micros(bigframes_df["timestamp_col"]).to_pandas()
52+
53+
expected_res = (
54+
pandas_df["timestamp_col"]
55+
.apply(lambda ts: _to_unix_epoch(ts, "us"))
56+
.astype("Int64")
57+
)
58+
pd.testing.assert_series_equal(actual_res, expected_res)
59+
60+
61+
def _to_unix_epoch(
62+
ts: pd.Timestamp, unit: typing.Literal["s", "ms", "us"]
63+
) -> typing.Optional[int]:
64+
if pd.isna(ts):
65+
return None
66+
return (ts - pd.Timestamp("1970-01-01", tz="UTC")) // pd.Timedelta(1, unit)

0 commit comments

Comments
 (0)