Skip to content

Commit 4b65e2f

Browse files
BUG: Timestamp.replace handle out-of-pydatetime range (#50348)
* BUG: Timestamp.replace handle out-of-pydatetime range * update test * fix repr for out-of-pydatetime bounds * update test * xfail npdev * Update pandas/_libs/tslibs/timestamps.pyx Co-authored-by: Marco Edward Gorelli <[email protected]> --------- Co-authored-by: Marco Edward Gorelli <[email protected]>
1 parent 3ea8389 commit 4b65e2f

File tree

3 files changed

+57
-12
lines changed

3 files changed

+57
-12
lines changed

pandas/_libs/tslibs/timestamps.pyx

+28-12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ shadows the python class, where we do any heavy lifting.
88
"""
99

1010
import warnings
11+
1112
cimport cython
1213

1314
import numpy as np
@@ -80,6 +81,7 @@ from pandas._libs.tslibs.nattype cimport (
8081
from pandas._libs.tslibs.np_datetime cimport (
8182
NPY_DATETIMEUNIT,
8283
NPY_FR_ns,
84+
check_dts_bounds,
8385
cmp_dtstructs,
8486
cmp_scalar,
8587
convert_reso,
@@ -1032,19 +1034,19 @@ cdef class _Timestamp(ABCTimestamp):
10321034
stamp = self._repr_base
10331035
zone = None
10341036

1035-
try:
1036-
stamp += self.strftime("%z")
1037-
except ValueError:
1038-
year2000 = self.replace(year=2000)
1039-
stamp += year2000.strftime("%z")
1037+
if self.tzinfo is not None:
1038+
try:
1039+
stamp += self.strftime("%z")
1040+
except ValueError:
1041+
year2000 = self.replace(year=2000)
1042+
stamp += year2000.strftime("%z")
10401043

1041-
if self.tzinfo:
10421044
zone = get_timezone(self.tzinfo)
1043-
try:
1044-
stamp += zone.strftime(" %%Z")
1045-
except AttributeError:
1046-
# e.g. tzlocal has no `strftime`
1047-
pass
1045+
try:
1046+
stamp += zone.strftime(" %%Z")
1047+
except AttributeError:
1048+
# e.g. tzlocal has no `strftime`
1049+
pass
10481050

10491051
tz = f", tz='{zone}'" if zone is not None else ""
10501052

@@ -2216,6 +2218,7 @@ default 'raise'
22162218
object k, v
22172219
datetime ts_input
22182220
tzinfo_type tzobj
2221+
_TSObject ts
22192222

22202223
# set to naive if needed
22212224
tzobj = self.tzinfo
@@ -2261,7 +2264,20 @@ default 'raise'
22612264
tzobj = tzinfo
22622265

22632266
# reconstruct & check bounds
2264-
if tzobj is not None and treat_tz_as_pytz(tzobj):
2267+
if tzobj is None:
2268+
# We can avoid going through pydatetime paths, which is robust
2269+
# to datetimes outside of pydatetime range.
2270+
ts = _TSObject()
2271+
check_dts_bounds(&dts, self._creso)
2272+
ts.value = npy_datetimestruct_to_datetime(self._creso, &dts)
2273+
ts.dts = dts
2274+
ts.creso = self._creso
2275+
ts.fold = fold
2276+
return create_timestamp_from_ts(
2277+
ts.value, dts, tzobj, fold, reso=self._creso
2278+
)
2279+
2280+
elif tzobj is not None and treat_tz_as_pytz(tzobj):
22652281
# replacing across a DST boundary may induce a new tzinfo object
22662282
# see GH#18319
22672283
ts_input = tzobj.localize(datetime(dts.year, dts.month, dts.day,

pandas/tests/scalar/timestamp/test_unary_ops.py

+14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from pandas._libs import lib
1414
from pandas._libs.tslibs import (
1515
NaT,
16+
OutOfBoundsDatetime,
1617
Timedelta,
1718
Timestamp,
1819
conversion,
@@ -363,6 +364,19 @@ def checker(res, ts, nanos):
363364
# --------------------------------------------------------------
364365
# Timestamp.replace
365366

367+
def test_replace_out_of_pydatetime_bounds(self):
368+
# GH#50348
369+
ts = Timestamp("2016-01-01").as_unit("ns")
370+
371+
msg = "Out of bounds nanosecond timestamp: 99999-01-01 00:00:00"
372+
with pytest.raises(OutOfBoundsDatetime, match=msg):
373+
ts.replace(year=99_999)
374+
375+
ts = ts.as_unit("ms")
376+
result = ts.replace(year=99_999)
377+
assert result.year == 99_999
378+
assert result._value == Timestamp(np.datetime64("99999-01-01", "ms"))._value
379+
366380
def test_replace_non_nano(self):
367381
ts = Timestamp._from_value_and_reso(
368382
91514880000000000, NpyDatetimeUnit.NPY_FR_us.value, None

pandas/tests/tseries/offsets/test_year.py

+15
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77

88
from datetime import datetime
99

10+
import numpy as np
1011
import pytest
1112

13+
from pandas.compat import is_numpy_dev
14+
15+
from pandas import Timestamp
1216
from pandas.tests.tseries.offsets.common import (
1317
assert_is_on_offset,
1418
assert_offset_equal,
@@ -317,3 +321,14 @@ def test_offset(self, case):
317321
def test_is_on_offset(self, case):
318322
offset, dt, expected = case
319323
assert_is_on_offset(offset, dt, expected)
324+
325+
326+
@pytest.mark.xfail(is_numpy_dev, reason="result year is 1973, unclear why")
327+
def test_add_out_of_pydatetime_range():
328+
# GH#50348 don't raise in Timestamp.replace
329+
ts = Timestamp(np.datetime64("-20000-12-31"))
330+
off = YearEnd()
331+
332+
result = ts + off
333+
expected = Timestamp(np.datetime64("-19999-12-31"))
334+
assert result == expected

0 commit comments

Comments
 (0)