Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,4 @@ Other
^^^^^

- Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`)
- :func:`Timestamp.replace` will now handle Daylight Savings transitions gracefully (:issue:`18319`)
16 changes: 13 additions & 3 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ from np_datetime cimport (reverse_ops, cmp_scalar, check_dts_bounds,
is_leapyear)
from timedeltas import Timedelta
from timedeltas cimport delta_to_nanoseconds
from timezones cimport get_timezone, is_utc, maybe_get_tz
from timezones cimport get_timezone, is_utc, maybe_get_tz, treat_tz_as_pytz

# ----------------------------------------------------------------------
# Constants
Expand Down Expand Up @@ -922,8 +922,18 @@ class Timestamp(_Timestamp):
_tzinfo = tzinfo

# reconstruct & check bounds
ts_input = datetime(dts.year, dts.month, dts.day, dts.hour, dts.min,
dts.sec, dts.us, tzinfo=_tzinfo)
if _tzinfo is not None and treat_tz_as_pytz(_tzinfo):
# replacing across a DST boundary may induce a new tzinfo object
# see GH#18319
ts_input = _tzinfo.localize(datetime(dts.year, dts.month, dts.day,
dts.hour, dts.min, dts.sec,
dts.us))
_tzinfo = ts_input.tzinfo
else:
ts_input = datetime(dts.year, dts.month, dts.day,
dts.hour, dts.min, dts.sec, dts.us,
tzinfo=_tzinfo)

ts = convert_datetime_to_tsobject(ts_input, _tzinfo)
value = ts.value + (dts.ps // 1000)
if value != NPY_NAT:
Expand Down
29 changes: 29 additions & 0 deletions pandas/tests/tseries/test_timezones.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ def tzstr(self, tz):
def localize(self, tz, x):
return tz.localize(x)

def normalize(self, ts):
tzinfo = ts.tzinfo
return tzinfo.normalize(ts)

def cmptz(self, tz1, tz2):
# Compare two timezones. Overridden in subclass to parameterize
# tests.
Expand Down Expand Up @@ -935,6 +939,27 @@ def test_datetimeindex_tz_nat(self):
assert isna(idx[1])
assert idx[0].tzinfo is not None

def test_replace_across_dst(self):
# GH#18319 check that 1) timezone is correctly normalized and
# 2) that hour is not incorrectly changed by this normalization
tz = self.tz('US/Eastern')

ts_naive = Timestamp('2017-12-03 16:03:30')
ts_aware = self.localize(tz, ts_naive)

# Preliminary sanity-check
assert ts_aware == self.normalize(ts_aware)

# Replace across DST boundary
ts2 = ts_aware.replace(month=6)

# Check that `replace` preserves hour literal
assert (ts2.hour, ts2.minute) == (ts_aware.hour, ts_aware.minute)

# Check that post-replace object is appropriately normalized
ts2b = self.normalize(ts2)
assert ts2 == ts2b


class TestTimeZoneSupportDateutil(TestTimeZoneSupportPytz):

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

def normalize(self, ts):
# no-op for dateutil
return ts

@td.skip_if_windows
def test_utc_with_system_utc(self):
from pandas._libs.tslibs.timezones import maybe_get_tz
Expand Down