Skip to content

Commit c0e3767

Browse files
jbrockmendeljreback
authored andcommitted
handle DST appropriately in Timestamp.replace (#18618)
1 parent 685813b commit c0e3767

File tree

3 files changed

+43
-3
lines changed

3 files changed

+43
-3
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,4 @@ Other
428428
^^^^^
429429

430430
- Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`)
431+
- :func:`Timestamp.replace` will now handle Daylight Savings transitions gracefully (:issue:`18319`)

pandas/_libs/tslibs/timestamps.pyx

+13-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ from np_datetime cimport (reverse_ops, cmp_scalar, check_dts_bounds,
3333
is_leapyear)
3434
from timedeltas import Timedelta
3535
from timedeltas cimport delta_to_nanoseconds
36-
from timezones cimport get_timezone, is_utc, maybe_get_tz
36+
from timezones cimport get_timezone, is_utc, maybe_get_tz, treat_tz_as_pytz
3737

3838
# ----------------------------------------------------------------------
3939
# Constants
@@ -922,8 +922,18 @@ class Timestamp(_Timestamp):
922922
_tzinfo = tzinfo
923923

924924
# reconstruct & check bounds
925-
ts_input = datetime(dts.year, dts.month, dts.day, dts.hour, dts.min,
926-
dts.sec, dts.us, tzinfo=_tzinfo)
925+
if _tzinfo is not None and treat_tz_as_pytz(_tzinfo):
926+
# replacing across a DST boundary may induce a new tzinfo object
927+
# see GH#18319
928+
ts_input = _tzinfo.localize(datetime(dts.year, dts.month, dts.day,
929+
dts.hour, dts.min, dts.sec,
930+
dts.us))
931+
_tzinfo = ts_input.tzinfo
932+
else:
933+
ts_input = datetime(dts.year, dts.month, dts.day,
934+
dts.hour, dts.min, dts.sec, dts.us,
935+
tzinfo=_tzinfo)
936+
927937
ts = convert_datetime_to_tsobject(ts_input, _tzinfo)
928938
value = ts.value + (dts.ps // 1000)
929939
if value != NPY_NAT:

pandas/tests/tseries/test_timezones.py

+29
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def tzstr(self, tz):
6161
def localize(self, tz, x):
6262
return tz.localize(x)
6363

64+
def normalize(self, ts):
65+
tzinfo = ts.tzinfo
66+
return tzinfo.normalize(ts)
67+
6468
def cmptz(self, tz1, tz2):
6569
# Compare two timezones. Overridden in subclass to parameterize
6670
# tests.
@@ -935,6 +939,27 @@ def test_datetimeindex_tz_nat(self):
935939
assert isna(idx[1])
936940
assert idx[0].tzinfo is not None
937941

942+
def test_replace_across_dst(self):
943+
# GH#18319 check that 1) timezone is correctly normalized and
944+
# 2) that hour is not incorrectly changed by this normalization
945+
tz = self.tz('US/Eastern')
946+
947+
ts_naive = Timestamp('2017-12-03 16:03:30')
948+
ts_aware = self.localize(tz, ts_naive)
949+
950+
# Preliminary sanity-check
951+
assert ts_aware == self.normalize(ts_aware)
952+
953+
# Replace across DST boundary
954+
ts2 = ts_aware.replace(month=6)
955+
956+
# Check that `replace` preserves hour literal
957+
assert (ts2.hour, ts2.minute) == (ts_aware.hour, ts_aware.minute)
958+
959+
# Check that post-replace object is appropriately normalized
960+
ts2b = self.normalize(ts2)
961+
assert ts2 == ts2b
962+
938963

939964
class TestTimeZoneSupportDateutil(TestTimeZoneSupportPytz):
940965

@@ -959,6 +984,10 @@ def cmptz(self, tz1, tz2):
959984
def localize(self, tz, x):
960985
return x.replace(tzinfo=tz)
961986

987+
def normalize(self, ts):
988+
# no-op for dateutil
989+
return ts
990+
962991
@td.skip_if_windows
963992
def test_utc_with_system_utc(self):
964993
from pandas._libs.tslibs.timezones import maybe_get_tz

0 commit comments

Comments
 (0)