Skip to content

Commit 1aff12b

Browse files
jrebackNo-Stream
authored andcommitted
BUG: in Timestamp.replace when replacing tzinfo around DST changes (pandas-dev#17507)
closes pandas-dev#15683
1 parent a67b1fd commit 1aff12b

File tree

4 files changed

+72
-21
lines changed

4 files changed

+72
-21
lines changed

asv_bench/benchmarks/timestamp.py

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from .pandas_vb_common import *
22
from pandas import to_timedelta, Timestamp
3+
import pytz
4+
import datetime
35

46

57
class TimestampProperties(object):
@@ -58,3 +60,24 @@ def time_is_leap_year(self):
5860

5961
def time_microsecond(self):
6062
self.ts.microsecond
63+
64+
65+
class TimestampOps(object):
66+
goal_time = 0.2
67+
68+
def setup(self):
69+
self.ts = Timestamp('2017-08-25 08:16:14')
70+
self.ts_tz = Timestamp('2017-08-25 08:16:14', tz='US/Eastern')
71+
72+
dt = datetime.datetime(2016, 3, 27, 1)
73+
self.tzinfo = pytz.timezone('CET').localize(dt, is_dst=False).tzinfo
74+
self.ts2 = Timestamp(dt)
75+
76+
def time_replace_tz(self):
77+
self.ts.replace(tzinfo=pytz.timezone('US/Eastern'))
78+
79+
def time_replace_across_dst(self):
80+
self.ts2.replace(tzinfo=self.tzinfo)
81+
82+
def time_replace_None(self):
83+
self.ts_tz.replace(tzinfo=None)

doc/source/whatsnew/v0.21.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ Conversion
487487
- Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`)
488488
- Bug in :func:`Series.fillna` returns frame when ``inplace=True`` and ``value`` is dict (:issue:`16156`)
489489
- Bug in :attr:`Timestamp.weekday_name` returning a UTC-based weekday name when localized to a timezone (:issue:`17354`)
490+
- Bug in ``Timestamp.replace`` when replacing ``tzinfo`` around DST changes (:issue:`15683`)
490491

491492
Indexing
492493
^^^^^^^^

pandas/_libs/tslib.pyx

+27-21
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def ints_to_pydatetime(ndarray[int64_t] arr, tz=None, freq=None, box=False):
142142

143143
cdef:
144144
Py_ssize_t i, n = len(arr)
145+
ndarray[int64_t] trans, deltas
145146
pandas_datetimestruct dts
146147
object dt
147148
int64_t value
@@ -417,8 +418,9 @@ class Timestamp(_Timestamp):
417418

418419
def _round(self, freq, rounder):
419420

420-
cdef int64_t unit
421-
cdef object result, value
421+
cdef:
422+
int64_t unit, r, value, buff = 1000000
423+
object result
422424

423425
from pandas.tseries.frequencies import to_offset
424426
unit = to_offset(freq).nanos
@@ -429,16 +431,15 @@ class Timestamp(_Timestamp):
429431
if unit < 1000 and unit % 1000 != 0:
430432
# for nano rounding, work with the last 6 digits separately
431433
# due to float precision
432-
buff = 1000000
433-
result = (buff * (value // buff) + unit *
434-
(rounder((value % buff) / float(unit))).astype('i8'))
434+
r = (buff * (value // buff) + unit *
435+
(rounder((value % buff) / float(unit))).astype('i8'))
435436
elif unit >= 1000 and unit % 1000 != 0:
436437
msg = 'Precision will be lost using frequency: {}'
437438
warnings.warn(msg.format(freq))
438-
result = (unit * rounder(value / float(unit)).astype('i8'))
439+
r = (unit * rounder(value / float(unit)).astype('i8'))
439440
else:
440-
result = (unit * rounder(value / float(unit)).astype('i8'))
441-
result = Timestamp(result, unit='ns')
441+
r = (unit * rounder(value / float(unit)).astype('i8'))
442+
result = Timestamp(r, unit='ns')
442443
if self.tz is not None:
443444
result = result.tz_localize(self.tz)
444445
return result
@@ -683,14 +684,16 @@ class Timestamp(_Timestamp):
683684

684685
cdef:
685686
pandas_datetimestruct dts
686-
int64_t value
687+
int64_t value, value_tz, offset
687688
object _tzinfo, result, k, v
689+
datetime ts_input
688690

689691
# set to naive if needed
690692
_tzinfo = self.tzinfo
691693
value = self.value
692694
if _tzinfo is not None:
693-
value = tz_convert_single(value, 'UTC', _tzinfo)
695+
value_tz = tz_convert_single(value, _tzinfo, 'UTC')
696+
value += value - value_tz
694697

695698
# setup components
696699
pandas_datetime_to_datetimestruct(value, PANDAS_FR_ns, &dts)
@@ -724,16 +727,14 @@ class Timestamp(_Timestamp):
724727
_tzinfo = tzinfo
725728

726729
# reconstruct & check bounds
727-
value = pandas_datetimestruct_to_datetime(PANDAS_FR_ns, &dts)
730+
ts_input = datetime(dts.year, dts.month, dts.day, dts.hour, dts.min,
731+
dts.sec, dts.us, tzinfo=_tzinfo)
732+
ts = convert_to_tsobject(ts_input, _tzinfo, None, 0, 0)
733+
value = ts.value + (dts.ps // 1000)
728734
if value != NPY_NAT:
729735
_check_dts_bounds(&dts)
730736

731-
# set tz if needed
732-
if _tzinfo is not None:
733-
value = tz_convert_single(value, _tzinfo, 'UTC')
734-
735-
result = create_timestamp_from_ts(value, dts, _tzinfo, self.freq)
736-
return result
737+
return create_timestamp_from_ts(value, dts, _tzinfo, self.freq)
737738

738739
def isoformat(self, sep='T'):
739740
base = super(_Timestamp, self).isoformat(sep=sep)
@@ -1175,7 +1176,7 @@ cdef class _Timestamp(datetime):
11751176
return np.datetime64(self.value, 'ns')
11761177

11771178
def __add__(self, other):
1178-
cdef int64_t other_int
1179+
cdef int64_t other_int, nanos
11791180

11801181
if is_timedelta64_object(other):
11811182
other_int = other.astype('timedelta64[ns]').view('i8')
@@ -1625,6 +1626,10 @@ cdef inline void _localize_tso(_TSObject obj, object tz):
16251626
"""
16261627
Take a TSObject in UTC and localizes to timezone tz.
16271628
"""
1629+
cdef:
1630+
ndarray[int64_t] trans, deltas
1631+
Py_ssize_t delta, posn
1632+
16281633
if is_utc(tz):
16291634
obj.tzinfo = tz
16301635
elif is_tzlocal(tz):
@@ -1676,7 +1681,7 @@ cdef inline void _localize_tso(_TSObject obj, object tz):
16761681
obj.tzinfo = tz
16771682

16781683

1679-
def _localize_pydatetime(object dt, object tz):
1684+
cpdef inline object _localize_pydatetime(object dt, object tz):
16801685
"""
16811686
Take a datetime/Timestamp in UTC and localizes to timezone tz.
16821687
"""
@@ -3892,7 +3897,7 @@ for _maybe_method_name in dir(NaTType):
38923897
# Conversion routines
38933898

38943899

3895-
def _delta_to_nanoseconds(delta):
3900+
cpdef int64_t _delta_to_nanoseconds(delta):
38963901
if isinstance(delta, np.ndarray):
38973902
return delta.astype('m8[ns]').astype('int64')
38983903
if hasattr(delta, 'nanos'):
@@ -4137,7 +4142,7 @@ def tz_convert(ndarray[int64_t] vals, object tz1, object tz2):
41374142
return result
41384143

41394144

4140-
def tz_convert_single(int64_t val, object tz1, object tz2):
4145+
cpdef int64_t tz_convert_single(int64_t val, object tz1, object tz2):
41414146
"""
41424147
Convert the val (in i8) from timezone1 to timezone2
41434148
@@ -5006,6 +5011,7 @@ cdef inline int64_t _normalized_stamp(pandas_datetimestruct *dts) nogil:
50065011
def dates_normalized(ndarray[int64_t] stamps, tz=None):
50075012
cdef:
50085013
Py_ssize_t i, n = len(stamps)
5014+
ndarray[int64_t] trans, deltas
50095015
pandas_datetimestruct dts
50105016

50115017
if tz is None or is_utc(tz):

pandas/tests/tseries/test_timezones.py

+21
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,27 @@ def test_ambiguous_compat(self):
12691269
assert (result_pytz.to_pydatetime().tzname() ==
12701270
result_dateutil.to_pydatetime().tzname())
12711271

1272+
def test_replace_tzinfo(self):
1273+
# GH 15683
1274+
dt = datetime(2016, 3, 27, 1)
1275+
tzinfo = pytz.timezone('CET').localize(dt, is_dst=False).tzinfo
1276+
1277+
result_dt = dt.replace(tzinfo=tzinfo)
1278+
result_pd = Timestamp(dt).replace(tzinfo=tzinfo)
1279+
1280+
if hasattr(result_dt, 'timestamp'): # New method in Py 3.3
1281+
assert result_dt.timestamp() == result_pd.timestamp()
1282+
assert result_dt == result_pd
1283+
assert result_dt == result_pd.to_pydatetime()
1284+
1285+
result_dt = dt.replace(tzinfo=tzinfo).replace(tzinfo=None)
1286+
result_pd = Timestamp(dt).replace(tzinfo=tzinfo).replace(tzinfo=None)
1287+
1288+
if hasattr(result_dt, 'timestamp'): # New method in Py 3.3
1289+
assert result_dt.timestamp() == result_pd.timestamp()
1290+
assert result_dt == result_pd
1291+
assert result_dt == result_pd.to_pydatetime()
1292+
12721293
def test_index_equals_with_tz(self):
12731294
left = date_range('1/1/2011', periods=100, freq='H', tz='utc')
12741295
right = date_range('1/1/2011', periods=100, freq='H', tz='US/Eastern')

0 commit comments

Comments
 (0)