diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index c4025c7e5efe7..9e9dab155a5cf 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -8,6 +8,7 @@ shadows the python class, where we do any heavy lifting. """ import warnings + cimport cython import numpy as np @@ -80,6 +81,7 @@ from pandas._libs.tslibs.nattype cimport ( from pandas._libs.tslibs.np_datetime cimport ( NPY_DATETIMEUNIT, NPY_FR_ns, + check_dts_bounds, cmp_dtstructs, cmp_scalar, convert_reso, @@ -1032,19 +1034,19 @@ cdef class _Timestamp(ABCTimestamp): stamp = self._repr_base zone = None - try: - stamp += self.strftime("%z") - except ValueError: - year2000 = self.replace(year=2000) - stamp += year2000.strftime("%z") + if self.tzinfo is not None: + try: + stamp += self.strftime("%z") + except ValueError: + year2000 = self.replace(year=2000) + stamp += year2000.strftime("%z") - if self.tzinfo: zone = get_timezone(self.tzinfo) - try: - stamp += zone.strftime(" %%Z") - except AttributeError: - # e.g. tzlocal has no `strftime` - pass + try: + stamp += zone.strftime(" %%Z") + except AttributeError: + # e.g. tzlocal has no `strftime` + pass tz = f", tz='{zone}'" if zone is not None else "" @@ -2216,6 +2218,7 @@ default 'raise' object k, v datetime ts_input tzinfo_type tzobj + _TSObject ts # set to naive if needed tzobj = self.tzinfo @@ -2261,7 +2264,20 @@ default 'raise' tzobj = tzinfo # reconstruct & check bounds - if tzobj is not None and treat_tz_as_pytz(tzobj): + if tzobj is None: + # We can avoid going through pydatetime paths, which is robust + # to datetimes outside of pydatetime range. + ts = _TSObject() + check_dts_bounds(&dts, self._creso) + ts.value = npy_datetimestruct_to_datetime(self._creso, &dts) + ts.dts = dts + ts.creso = self._creso + ts.fold = fold + return create_timestamp_from_ts( + ts.value, dts, tzobj, fold, reso=self._creso + ) + + elif tzobj is not None and treat_tz_as_pytz(tzobj): # replacing across a DST boundary may induce a new tzinfo object # see GH#18319 ts_input = tzobj.localize(datetime(dts.year, dts.month, dts.day, diff --git a/pandas/tests/scalar/timestamp/test_unary_ops.py b/pandas/tests/scalar/timestamp/test_unary_ops.py index 62bfede7f4261..be24fd7da8591 100644 --- a/pandas/tests/scalar/timestamp/test_unary_ops.py +++ b/pandas/tests/scalar/timestamp/test_unary_ops.py @@ -13,6 +13,7 @@ from pandas._libs import lib from pandas._libs.tslibs import ( NaT, + OutOfBoundsDatetime, Timedelta, Timestamp, conversion, @@ -363,6 +364,19 @@ def checker(res, ts, nanos): # -------------------------------------------------------------- # Timestamp.replace + def test_replace_out_of_pydatetime_bounds(self): + # GH#50348 + ts = Timestamp("2016-01-01").as_unit("ns") + + msg = "Out of bounds nanosecond timestamp: 99999-01-01 00:00:00" + with pytest.raises(OutOfBoundsDatetime, match=msg): + ts.replace(year=99_999) + + ts = ts.as_unit("ms") + result = ts.replace(year=99_999) + assert result.year == 99_999 + assert result._value == Timestamp(np.datetime64("99999-01-01", "ms"))._value + def test_replace_non_nano(self): ts = Timestamp._from_value_and_reso( 91514880000000000, NpyDatetimeUnit.NPY_FR_us.value, None diff --git a/pandas/tests/tseries/offsets/test_year.py b/pandas/tests/tseries/offsets/test_year.py index daa5171af2452..480c875c36e04 100644 --- a/pandas/tests/tseries/offsets/test_year.py +++ b/pandas/tests/tseries/offsets/test_year.py @@ -7,8 +7,12 @@ from datetime import datetime +import numpy as np import pytest +from pandas.compat import is_numpy_dev + +from pandas import Timestamp from pandas.tests.tseries.offsets.common import ( assert_is_on_offset, assert_offset_equal, @@ -317,3 +321,14 @@ def test_offset(self, case): def test_is_on_offset(self, case): offset, dt, expected = case assert_is_on_offset(offset, dt, expected) + + +@pytest.mark.xfail(is_numpy_dev, reason="result year is 1973, unclear why") +def test_add_out_of_pydatetime_range(): + # GH#50348 don't raise in Timestamp.replace + ts = Timestamp(np.datetime64("-20000-12-31")) + off = YearEnd() + + result = ts + off + expected = Timestamp(np.datetime64("-19999-12-31")) + assert result == expected