diff --git a/bigframes/core/indexes/datetimes.py b/bigframes/core/indexes/datetimes.py index 23ad8b03b4..b53c49b808 100644 --- a/bigframes/core/indexes/datetimes.py +++ b/bigframes/core/indexes/datetimes.py @@ -19,6 +19,7 @@ from bigframes_vendored.pandas.core.indexes import ( datetimes as vendored_pandas_datetime_index, ) +import pandas from bigframes.core import expression as ex from bigframes.core.indexes.base import Index @@ -54,3 +55,38 @@ def day_of_week(self) -> Index: @property def weekday(self) -> Index: return self.dayofweek + + +def date_range( + session, + start=None, + end=None, + periods=None, + freq=None, + tz=None, + normalize: bool = False, + name=None, + inclusive="both", + *, + unit: str | None = None, +) -> DatetimeIndex: + kwargs = {} + if unit is not None: + kwargs["unit"] = unit + + pd_index = pandas.date_range( + start=start, + end=end, + periods=periods, + freq=freq, + tz=tz, + normalize=normalize, + name=name, + inclusive=inclusive, + **kwargs, # type: ignore + ) + + return session.read_pandas(pd_index) + + +date_range.__doc__ = vendored_pandas_datetime_index.date_range.__doc__ diff --git a/bigframes/pandas/__init__.py b/bigframes/pandas/__init__.py index 2ea10132bc..a617b599b0 100644 --- a/bigframes/pandas/__init__.py +++ b/bigframes/pandas/__init__.py @@ -37,6 +37,7 @@ import bigframes.enums import bigframes.functions._utils as bff_utils from bigframes.pandas.core.api import to_timedelta +from bigframes.pandas.core.indexes.datetimes import date_range from bigframes.pandas.io.api import ( _read_gbq_colab, from_glob_path, @@ -371,6 +372,7 @@ def reset_session(): clean_up_by_session_id, concat, cut, + date_range, deploy_remote_function, deploy_udf, get_default_session_id, diff --git a/bigframes/pandas/core/indexes/__init__.py b/bigframes/pandas/core/indexes/__init__.py new file mode 100644 index 0000000000..0a2669d7a2 --- /dev/null +++ b/bigframes/pandas/core/indexes/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/bigframes/pandas/core/indexes/datetimes.py b/bigframes/pandas/core/indexes/datetimes.py new file mode 100644 index 0000000000..2e81fd85d0 --- /dev/null +++ b/bigframes/pandas/core/indexes/datetimes.py @@ -0,0 +1,50 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import inspect + +import bigframes.core.global_session as global_session +import bigframes.core.indexes +import bigframes.session + + +def date_range( + start=None, + end=None, + periods=None, + freq=None, + tz=None, + normalize: bool = False, + name=None, + inclusive="both", + *, + unit: str | None = None, +) -> bigframes.core.indexes.DatetimeIndex: + return global_session.with_default_session( + bigframes.session.Session.date_range, + start=start, + end=end, + periods=periods, + freq=freq, + tz=tz, + normalize=normalize, + name=name, + inclusive=inclusive, + unit=unit, + ) + + +date_range.__doc__ = inspect.getdoc(bigframes.session.Session.date_range) diff --git a/bigframes/session/__init__.py b/bigframes/session/__init__.py index f0cec864b4..559a20a35e 100644 --- a/bigframes/session/__init__.py +++ b/bigframes/session/__init__.py @@ -67,10 +67,8 @@ import bigframes.constants import bigframes.core from bigframes.core import blocks, log_adapter, utils +import bigframes.core.indexes.datetimes import bigframes.core.pyformat - -# Even though the ibis.backends.bigquery import is unused, it's needed -# to register new and replacement ops with the Ibis BigQuery backend. import bigframes.functions._function_session as bff_session import bigframes.functions.function as bff from bigframes.session import bigquery_session, bq_caching_executor, executor @@ -377,6 +375,8 @@ def close(self): self.bqclient, self.cloudfunctionsclient, self.session_id ) + date_range = bigframes.core.indexes.datetimes.date_range + @overload def read_gbq( # type: ignore[overload-overlap] self, diff --git a/third_party/bigframes_vendored/pandas/core/indexes/datetimes.py b/third_party/bigframes_vendored/pandas/core/indexes/datetimes.py index 105a376728..b00b6b8095 100644 --- a/third_party/bigframes_vendored/pandas/core/indexes/datetimes.py +++ b/third_party/bigframes_vendored/pandas/core/indexes/datetimes.py @@ -104,3 +104,185 @@ def weekday(self) -> base.Index: Index([5], dtype='Int64') """ raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE) + + +def date_range( + start=None, + end=None, + periods=None, + freq=None, + tz=None, + normalize: bool = False, + name=None, + inclusive="both", + *, + unit: str | None = None, + **kwargs, +) -> DatetimeIndex: + """ + Return a fixed frequency DatetimeIndex. + + Returns the range of equally spaced time points (where the difference between any + two adjacent points is specified by the given frequency) such that they fall in the + range `[start, end]` , where the first one and the last one are, resp., the first + and last time points in that range that fall on the boundary of ``freq`` (if given + as a frequency string) or that are valid for ``freq`` (if given as a + :class:`pandas.tseries.offsets.DateOffset`). If ``freq`` is positive, the points + satisfy `start <[=] x <[=] end`, and if ``freq`` is negative, the points satisfy + `end <[=] x <[=] start`. (If exactly one of ``start``, ``end``, or ``freq`` is *not* + specified, this missing parameter can be computed given ``periods``, the number of + timesteps in the range.) + + Of the four parameters ``start``, ``end``, ``periods``, and ``freq``, + exactly three must be specified. If ``freq`` is omitted, the resulting + ``DatetimeIndex`` will have ``periods`` linearly spaced elements between + ``start`` and ``end`` (closed on both sides). + + To learn more about the frequency strings, please see + :ref:`this link`. + + **Examples:** + + >>> import bigframes.pandas as bpd + >>> import pandas as pd + >>> bpd.options.display.progress_bar = None + + **Specifying the values** + + The next four examples generate the same `DatetimeIndex`, but vary + the combination of `start`, `end` and `periods`. + + Specify `start` and `end`, with the default daily frequency. + + >>> bpd.date_range(start="1/1/2018", end="1/08/2018") + DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04', + '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08'], + dtype='datetime64[ns]', freq='D') + + Specify timezone-aware `start` and `end`, with the default daily frequency. + + >>> bpd.date_range( + ... start=pd.to_datetime("1/1/2018").tz_localize("Europe/Berlin"), + ... end=pd.to_datetime("1/08/2018").tz_localize("Europe/Berlin"), + ... ) + DatetimeIndex(['2018-01-01 00:00:00+01:00', '2018-01-02 00:00:00+01:00', + '2018-01-03 00:00:00+01:00', '2018-01-04 00:00:00+01:00', + '2018-01-05 00:00:00+01:00', '2018-01-06 00:00:00+01:00', + '2018-01-07 00:00:00+01:00', '2018-01-08 00:00:00+01:00'], + dtype='datetime64[ns, Europe/Berlin]', freq='D') + + Specify `start` and `periods`, the number of periods (days). + + >>> bpd.date_range(start="1/1/2018", periods=8) + DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04', + '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08'], + dtype='datetime64[ns]', freq='D') + + Specify `end` and `periods`, the number of periods (days). + + >>> bpd.date_range(end="1/1/2018", periods=8) + DatetimeIndex(['2017-12-25', '2017-12-26', '2017-12-27', '2017-12-28', + '2017-12-29', '2017-12-30', '2017-12-31', '2018-01-01'], + dtype='datetime64[ns]', freq='D') + + Specify `start`, `end`, and `periods`; the frequency is generated + automatically (linearly spaced). + + >>> bpd.date_range(start="2018-04-24", end="2018-04-27", periods=3) + DatetimeIndex(['2018-04-24 00:00:00', '2018-04-25 12:00:00', + '2018-04-27 00:00:00'], + dtype='datetime64[ns]', freq=None) + + **Other Parameters** + + Changed the `freq` (frequency) to ``'ME'`` (month end frequency). + + >>> bpd.date_range(start="1/1/2018", periods=5, freq="ME") + DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31', '2018-04-30', + '2018-05-31'], + dtype='datetime64[ns]', freq='ME') + + Multiples are allowed + + >>> bpd.date_range(start="1/1/2018", periods=5, freq="3ME") + DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31', + '2019-01-31'], + dtype='datetime64[ns]', freq='3ME') + + `freq` can also be specified as an Offset object. + + >>> bpd.date_range(start="1/1/2018", periods=5, freq=pd.offsets.MonthEnd(3)) + DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31', + '2019-01-31'], + dtype='datetime64[ns]', freq='3ME') + + Specify `tz` to set the timezone. + + >>> bpd.date_range(start="1/1/2018", periods=5, tz="Asia/Tokyo") + DatetimeIndex(['2018-01-01 00:00:00+09:00', '2018-01-02 00:00:00+09:00', + '2018-01-03 00:00:00+09:00', '2018-01-04 00:00:00+09:00', + '2018-01-05 00:00:00+09:00'], + dtype='datetime64[ns, Asia/Tokyo]', freq='D') + + `inclusive` controls whether to include `start` and `end` that are on the + boundary. The default, "both", includes boundary points on either end. + + >>> bpd.date_range(start="2017-01-01", end="2017-01-04", inclusive="both") + DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03', '2017-01-04'], + dtype='datetime64[ns]', freq='D') + + Use ``inclusive='left'`` to exclude `end` if it falls on the boundary. + + >>> bpd.date_range(start="2017-01-01", end="2017-01-04", inclusive="left") + DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'], + dtype='datetime64[ns]', freq='D') + + Use ``inclusive='right'`` to exclude `start` if it falls on the boundary, and + similarly ``inclusive='neither'`` will exclude both `start` and `end`. + + >>> bpd.date_range(start="2017-01-01", end="2017-01-04", inclusive="right") + DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'], + dtype='datetime64[ns]', freq='D') + + **Specify a unit** + + >>> bpd.date_range(start="2017-01-01", periods=10, freq="100YS", unit="s") + DatetimeIndex(['2017-01-01', '2117-01-01', '2217-01-01', '2317-01-01', + '2417-01-01', '2517-01-01', '2617-01-01', '2717-01-01', + '2817-01-01', '2917-01-01'], + dtype='datetime64[s]', freq='100YS-JAN') + + Arguments: + start (str or datetime-like, optional): + Left bound for generating dates. + end (str or datetime-like, optional): + Right bound for generating dates. + periods (int, optional): + Number of periods to generate. + freq (str, Timedelta, datetime.timedelta, or DateOffset, default 'D'): + Frequency strings can have multiples, e.g. '5h'. See + :ref:`here ` for a list of + frequency aliases. + tz (str or tzinfo, optional): + Time zone name for returning localized DatetimeIndex. By default, + the resulting DatetimeIndex is timezone-naive unless timezone-aware + datetime-likes are passed. + + "UTC" is the only currently-supported timezone. + normalize (bool, default False): + Normalize start/end dates to midnight before generating date range. + name (str, default None): + Name of the resulting DatetimeIndex. + inclusive ({"both", "neither", "left", "right"}, default "both"): + Include boundaries; Whether to set each bound as closed or open. + unit (str, default None): + Specify the desired resolution of the result. + + "us" is the only currently-supported resolution. + + Returns: + DatetimeIndex + A DatetimeIndex object of the generated dates. + """ + + raise NotImplementedError(constants.ABSTRACT_METHOD_ERROR_MESSAGE)