Skip to content

Commit 13143d9

Browse files
authored
ENH: handle nonexistent shift for ZoneInfo (#50219)
* ENH: handle nonexistent shift for ZoneInfo * add test with shift_backward
1 parent 179dc14 commit 13143d9

File tree

3 files changed

+22
-23
lines changed

3 files changed

+22
-23
lines changed

pandas/_libs/tslibs/tzconversion.pyx

+20-6
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ timedelta-like}
240240
str stamp
241241
Localizer info = Localizer(tz, creso=creso)
242242
int64_t pph = periods_per_day(creso) // 24
243+
int64_t pps = periods_per_second(creso)
244+
npy_datetimestruct dts
243245

244246
# Vectorized version of DstTzInfo.localize
245247
if info.use_utc:
@@ -388,14 +390,26 @@ timedelta-like}
388390
new_local = val - remaining_mins - 1
389391

390392
if is_zi:
391-
raise NotImplementedError(
392-
"nonexistent shifting is not implemented with ZoneInfo tzinfos"
393-
)
393+
# use the same construction as in _get_utc_bounds_zoneinfo
394+
pandas_datetime_to_datetimestruct(new_local, creso, &dts)
395+
extra = (dts.ps // 1000) * (pps // 1_000_000_000)
396+
397+
dt = datetime_new(dts.year, dts.month, dts.day, dts.hour,
398+
dts.min, dts.sec, dts.us, None)
394399

395-
delta_idx = bisect_right_i8(info.tdata, new_local, info.ntrans)
400+
if shift_forward or shift_delta > 0:
401+
dt = dt.replace(tzinfo=tz, fold=1)
402+
else:
403+
dt = dt.replace(tzinfo=tz, fold=0)
404+
dt = dt.astimezone(utc_stdlib)
405+
dt = dt.replace(tzinfo=None)
406+
result[i] = pydatetime_to_dt64(dt, &dts, creso) + extra
407+
408+
else:
409+
delta_idx = bisect_right_i8(info.tdata, new_local, info.ntrans)
396410

397-
delta_idx = delta_idx - delta_idx_offset
398-
result[i] = new_local - info.deltas[delta_idx]
411+
delta_idx = delta_idx - delta_idx_offset
412+
result[i] = new_local - info.deltas[delta_idx]
399413
elif fill_nonexist:
400414
result[i] = NPY_NAT
401415
else:

pandas/tests/indexes/datetimes/test_constructors.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -880,15 +880,8 @@ def test_constructor_with_ambiguous_keyword_arg(self):
880880
result = date_range(end=end, periods=2, ambiguous=False)
881881
tm.assert_index_equal(result, expected)
882882

883-
def test_constructor_with_nonexistent_keyword_arg(self, warsaw, request):
883+
def test_constructor_with_nonexistent_keyword_arg(self, warsaw):
884884
# GH 35297
885-
if type(warsaw).__name__ == "ZoneInfo":
886-
mark = pytest.mark.xfail(
887-
reason="nonexistent-shift not yet implemented for ZoneInfo",
888-
raises=NotImplementedError,
889-
)
890-
request.node.add_marker(mark)
891-
892885
timezone = warsaw
893886

894887
# nonexistent keyword in start

pandas/tests/series/methods/test_tz_localize.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def test_series_tz_localize_matching_index(self):
6464
"method, exp",
6565
[
6666
["shift_forward", "2015-03-29 03:00:00"],
67+
["shift_backward", "2015-03-29 01:59:59.999999999"],
6768
["NaT", NaT],
6869
["raise", None],
6970
["foo", "invalid"],
@@ -99,15 +100,6 @@ def test_tz_localize_nonexistent(self, warsaw, method, exp):
99100
with pytest.raises(ValueError, match=msg):
100101
df.tz_localize(tz, nonexistent=method)
101102

102-
elif method == "shift_forward" and type(tz).__name__ == "ZoneInfo":
103-
msg = "nonexistent shifting is not implemented with ZoneInfo tzinfos"
104-
with pytest.raises(NotImplementedError, match=msg):
105-
ser.tz_localize(tz, nonexistent=method)
106-
with pytest.raises(NotImplementedError, match=msg):
107-
df.tz_localize(tz, nonexistent=method)
108-
with pytest.raises(NotImplementedError, match=msg):
109-
dti.tz_localize(tz, nonexistent=method)
110-
111103
else:
112104
result = ser.tz_localize(tz, nonexistent=method)
113105
expected = Series(1, index=DatetimeIndex([exp] * n, tz=tz))

0 commit comments

Comments
 (0)