diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst index 032bcf09244e5..51ec4361a171e 100644 --- a/doc/source/whatsnew/v2.0.0.rst +++ b/doc/source/whatsnew/v2.0.0.rst @@ -610,6 +610,8 @@ Datetimelike - Bug in :class:`DatetimeIndex` constructor failing to raise when ``tz=None`` is explicitly specified in conjunction with timezone-aware ``dtype`` or data (:issue:`48659`) - Bug in subtracting a ``datetime`` scalar from :class:`DatetimeIndex` failing to retain the original ``freq`` attribute (:issue:`48818`) - Bug in ``pandas.tseries.holiday.Holiday`` where a half-open date interval causes inconsistent return types from :meth:`USFederalHolidayCalendar.holidays` (:issue:`49075`) +- Bug in rendering :class:`DatetimeIndex` and :class:`Series` and :class:`DataFrame` with timezone-aware dtypes with ``dateutil`` or ``zoneinfo`` timezones near daylight-savings transitions (:issue:`49684`) +- Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/vectorized.pyi b/pandas/_libs/tslibs/vectorized.pyi index 22f457b9ddc0b..3fd9e2501e611 100644 --- a/pandas/_libs/tslibs/vectorized.pyi +++ b/pandas/_libs/tslibs/vectorized.pyi @@ -33,7 +33,6 @@ def get_resolution( def ints_to_pydatetime( arr: npt.NDArray[np.int64], tz: tzinfo | None = ..., - fold: bool = ..., box: str = ..., reso: int = ..., # NPY_DATETIMEUNIT ) -> npt.NDArray[np.object_]: ... diff --git a/pandas/_libs/tslibs/vectorized.pyx b/pandas/_libs/tslibs/vectorized.pyx index 8661ba4b9b2f1..b95cebd60a847 100644 --- a/pandas/_libs/tslibs/vectorized.pyx +++ b/pandas/_libs/tslibs/vectorized.pyx @@ -94,7 +94,6 @@ def tz_convert_from_utc(ndarray stamps, tzinfo tz, NPY_DATETIMEUNIT reso=NPY_FR_ def ints_to_pydatetime( ndarray stamps, tzinfo tz=None, - bint fold=False, str box="datetime", NPY_DATETIMEUNIT reso=NPY_FR_ns, ) -> np.ndarray: @@ -136,6 +135,7 @@ def ints_to_pydatetime( tzinfo new_tz bint use_date = False, use_ts = False, use_pydt = False object res_val + bint fold = 0 # Note that `result` (and thus `result_flat`) is C-order and # `it` iterates C-order as well, so the iteration matches @@ -168,7 +168,7 @@ def ints_to_pydatetime( else: - local_val = info.utc_val_to_local_val(utc_val, &pos) + local_val = info.utc_val_to_local_val(utc_val, &pos, &fold) if info.use_pytz: # find right representation of dst etc in pytz timezone new_tz = tz._tzinfos[tz._transition_info[pos]] diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index 166362a9a8c30..728a3753da785 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -4,6 +4,11 @@ from datetime import timedelta import operator +try: + from zoneinfo import ZoneInfo +except ImportError: + ZoneInfo = None + import numpy as np import pytest @@ -706,3 +711,38 @@ def test_tz_localize_t2d(self): roundtrip = expected.tz_localize("US/Pacific") tm.assert_datetime_array_equal(roundtrip, dta) + + easts = ["US/Eastern", "dateutil/US/Eastern"] + if ZoneInfo is not None: + try: + easts.append(ZoneInfo("US/Eastern")) + except KeyError: + # No tzdata + pass + + @pytest.mark.parametrize("tz", easts) + def test_iter_zoneinfo_fold(self, tz): + # GH#49684 + utc_vals = np.array( + [1320552000, 1320555600, 1320559200, 1320562800], dtype=np.int64 + ) + utc_vals *= 1_000_000_000 + + dta = DatetimeArray(utc_vals).tz_localize("UTC").tz_convert(tz) + + left = dta[2] + right = list(dta)[2] + assert str(left) == str(right) + # previously there was a bug where with non-pytz right would be + # Timestamp('2011-11-06 01:00:00-0400', tz='US/Eastern') + # while left would be + # Timestamp('2011-11-06 01:00:00-0500', tz='US/Eastern') + # The .value's would match (so they would compare as equal), + # but the folds would not + assert left.utcoffset() == right.utcoffset() + + # The same bug in ints_to_pydatetime affected .astype, so we test + # that here. + right2 = dta.astype(object)[2] + assert str(left) == str(right2) + assert left.utcoffset() == right2.utcoffset()