diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt index fcadd26156b1d..b6ca7b5ec50b3 100644 --- a/doc/source/whatsnew/v0.21.0.txt +++ b/doc/source/whatsnew/v0.21.0.txt @@ -342,6 +342,7 @@ Conversion - Fixed the return type of ``IntervalIndex.is_non_overlapping_monotonic`` to be a Python ``bool`` for consistency with similar attributes/methods. Previously returned a ``numpy.bool_``. (:issue:`17237`) - Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`) - Bug in :func:`Series.fillna` returns frame when ``inplace=True`` and ``value`` is dict (:issue:`16156`) +- Bug in ``Timestamp.replace`` when replacing ``tzinfo`` around DST changes (:issue:`15683`) Indexing ^^^^^^^^ diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index b5aca2e3ec309..9ed849f34c9a0 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -700,14 +700,16 @@ class Timestamp(_Timestamp): cdef: pandas_datetimestruct dts - int64_t value - object _tzinfo, result, k, v + int64_t value, value_tz, offset + object _tzinfo, result, k, v, ts_input # set to naive if needed _tzinfo = self.tzinfo value = self.value if _tzinfo is not None: - value = tz_convert_single(value, 'UTC', _tzinfo) + value_tz = tz_convert_single(value, _tzinfo, 'UTC') + offset = value - value_tz + value += offset # setup components pandas_datetime_to_datetimestruct(value, PANDAS_FR_ns, &dts) @@ -741,16 +743,14 @@ class Timestamp(_Timestamp): _tzinfo = tzinfo # reconstruct & check bounds - value = pandas_datetimestruct_to_datetime(PANDAS_FR_ns, &dts) + ts_input = datetime(dts.year, dts.month, dts.day, dts.hour, dts.min, + dts.sec, dts.us, tzinfo=_tzinfo) + ts = convert_to_tsobject(ts_input, _tzinfo, None, 0, 0) + value = ts.value + (dts.ps // 1000) if value != NPY_NAT: _check_dts_bounds(&dts) - # set tz if needed - if _tzinfo is not None: - value = tz_convert_single(value, _tzinfo, 'UTC') - - result = create_timestamp_from_ts(value, dts, _tzinfo, self.freq) - return result + return create_timestamp_from_ts(value, dts, _tzinfo, self.freq) def isoformat(self, sep='T'): base = super(_Timestamp, self).isoformat(sep=sep) diff --git a/pandas/tests/tseries/test_timezones.py b/pandas/tests/tseries/test_timezones.py index a9ecfd797a32b..ac1a338d2844d 100644 --- a/pandas/tests/tseries/test_timezones.py +++ b/pandas/tests/tseries/test_timezones.py @@ -1269,6 +1269,27 @@ def test_ambiguous_compat(self): assert (result_pytz.to_pydatetime().tzname() == result_dateutil.to_pydatetime().tzname()) + def test_replace_tzinfo(self): + # GH 15683 + dt = datetime(2016, 3, 27, 1) + tzinfo = pytz.timezone('CET').localize(dt, is_dst=False).tzinfo + + result_dt = dt.replace(tzinfo=tzinfo) + result_pd = Timestamp(dt).replace(tzinfo=tzinfo) + + if hasattr(result_dt, 'timestamp'): # New method in Py 3.3 + assert result_dt.timestamp() == result_pd.timestamp() + assert result_dt == result_pd + assert result_dt == result_pd.to_pydatetime() + + result_dt = dt.replace(tzinfo=tzinfo).replace(tzinfo=None) + result_pd = Timestamp(dt).replace(tzinfo=tzinfo).replace(tzinfo=None) + + if hasattr(result_dt, 'timestamp'): # New method in Py 3.3 + assert result_dt.timestamp() == result_pd.timestamp() + assert result_dt == result_pd + assert result_dt == result_pd.to_pydatetime() + def test_index_equals_with_tz(self): left = date_range('1/1/2011', periods=100, freq='H', tz='utc') right = date_range('1/1/2011', periods=100, freq='H', tz='US/Eastern')