diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index a2198d9103528..fac007febd29f 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -716,7 +716,7 @@ Datetimelike - Bug in :class:`Timestamp` and :func:`to_datetime` where a string representing a barely out-of-bounds timestamp would be incorrectly rounded down instead of raising ``OutOfBoundsDatetime`` (:issue:`19382`) - Bug in :func:`Timestamp.floor` :func:`DatetimeIndex.floor` where time stamps far in the future and past were not rounded correctly (:issue:`19206`) - Bug in :func:`to_datetime` where passing an out-of-bounds datetime with ``errors='coerce'`` and ``utc=True`` would raise ``OutOfBoundsDatetime`` instead of parsing to ``NaT`` (:issue:`19612`) -- +- Bug in :func:`date_range` with frequency of ``Day`` or higher where dates sufficiently far in the future could wrap around to the past instead of raising ``OutOfBoundsDatetime`` (:issue:`19740`) Timezones ^^^^^^^^^ diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index cc9ce1f3fd5eb..0d86b32c719fe 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -2137,11 +2137,11 @@ def _generate_regular_range(start, end, periods, offset): tz = start.tz elif start is not None: b = Timestamp(start).value - e = b + np.int64(periods) * stride + e = _reraise_overflow_as_oob(b, periods, stride, side='start') tz = start.tz elif end is not None: e = Timestamp(end).value + stride - b = e - np.int64(periods) * stride + b = _reraise_overflow_as_oob(e, periods, stride, side='end') tz = end.tz else: raise ValueError("at least 'start' or 'end' should be specified " @@ -2166,6 +2166,44 @@ def _generate_regular_range(start, end, periods, offset): return data +def _reraise_overflow_as_oob(endpoint, periods, stride, side='start'): + """ + Calculate the second endpoint for passing to np.arange, checking + to avoid an integer overflow. Catch OverflowError and re-raise + as OutOfBoundsDatetime. + + Parameters + ---------- + endpoint : int + periods : int + stride : int + side : {'start', 'end'} + + Returns + ------- + other_end : int + + Raises + ------ + OutOfBoundsDatetime + """ + # GH#19740 raise instead of incorrectly wrapping around + assert side in ['start', 'end'] + if side == 'end': + stride *= -1 + + try: + other_end = checked_add_with_arr(np.int64(endpoint), + np.int64(periods) * stride) + except OverflowError: + raise libts.OutOfBoundsDatetime('Cannot generate range with ' + '{side}={endpoint} and ' + 'periods={periods}' + .format(side=side, endpoint=endpoint, + periods=periods)) + return other_end + + def date_range(start=None, end=None, periods=None, freq='D', tz=None, normalize=False, name=None, closed=None, **kwargs): """ diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 3738398d017f8..dda68335e03b2 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -13,6 +13,7 @@ import pandas.util.testing as tm import pandas.util._test_decorators as td from pandas import compat +from pandas.errors import OutOfBoundsDatetime from pandas import date_range, bdate_range, offsets, DatetimeIndex, Timestamp from pandas.tseries.offsets import (generate_range, CDay, BDay, DateOffset, MonthEnd, prefix_mapping) @@ -78,6 +79,12 @@ def test_date_range_timestamp_equiv_preserve_frequency(self): class TestDateRanges(TestData): + def test_date_range_out_of_bounds(self): + # GH#19740 + with pytest.raises(OutOfBoundsDatetime): + date_range('2016-01-01', periods=100000, freq='D') + with pytest.raises(OutOfBoundsDatetime): + date_range(end='1763-10-12', periods=100000, freq='D') def test_date_range_gen_error(self): rng = date_range('1/1/2000 00:00', '1/1/2000 00:18', freq='5min')