diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 59c90534beefd..1a7fe0a24665d 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -92,9 +92,10 @@ Categorical Datetimelike ^^^^^^^^^^^^ + - Bug in :class:`Timestamp` where constructing :class:`Timestamp` from ambiguous epoch time and calling constructor again changed :meth:`Timestamp.value` property (:issue:`24329`) - :meth:`DatetimeArray.searchsorted`, :meth:`TimedeltaArray.searchsorted`, :meth:`PeriodArray.searchsorted` not recognizing non-pandas scalars and incorrectly raising ``ValueError`` instead of ``TypeError`` (:issue:`30950`) -- +- Bug in :class:`Timestamp` where constructing :class:`Timestamp` with dateutil timezone less than 128 nanoseconds before daylight saving time switch from winter to summer would result in nonexistent time (:issue:`31043`) Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 67c0f0cc33ab8..61b2721696bad 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -278,7 +278,7 @@ cdef class _NaT(datetime): def total_seconds(self): """ - Total duration of timedelta in seconds (to ns precision). + Total duration of timedelta in seconds (to microsecond precision). """ # GH#10939 return np.nan diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 0a773b8a215ed..8f78ae6a1322e 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -861,9 +861,11 @@ cdef class _Timedelta(timedelta): def total_seconds(self): """ - Total duration of timedelta in seconds (to ns precision). + Total duration of timedelta in seconds (to microsecond precision). """ - return self.value / 1e9 + # GH 31043 + # Microseconds precision to avoid confusing tzinfo.utcoffset + return (self.value - self.value % 1000) / 1e9 def view(self, dtype): """ diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index df64820777f3f..c785eb67e5184 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -2,6 +2,7 @@ Tests for DatetimeIndex timezone-related methods """ from datetime import date, datetime, time, timedelta, tzinfo +from distutils.version import LooseVersion import dateutil from dateutil.tz import gettz, tzlocal @@ -10,6 +11,7 @@ import pytz from pandas._libs.tslibs import conversion, timezones +from pandas.compat._optional import _get_version import pandas.util._test_decorators as td import pandas as pd @@ -585,7 +587,10 @@ def test_dti_construction_ambiguous_endpoint(self, tz): "dateutil/US/Pacific", "shift_backward", "2019-03-10 01:00", - marks=pytest.mark.xfail(reason="GH 24329"), + marks=pytest.mark.xfail( + LooseVersion(_get_version(dateutil)) < LooseVersion("2.7.0"), + reason="GH 31043", + ), ), ["US/Pacific", timedelta(hours=1), "2019-03-10 03:00"], ], diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index c60406fdbc8a6..b70948318e39e 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -2,6 +2,7 @@ import calendar from datetime import datetime, timedelta +from distutils.version import LooseVersion import locale import unicodedata @@ -1092,3 +1093,23 @@ def test_constructor_ambigous_dst(): expected = ts.value result = Timestamp(ts).value assert result == expected + + +@pytest.mark.xfail( + LooseVersion(compat._optional._get_version(dateutil)) < LooseVersion("2.7.0"), + reason="dateutil moved to Timedelta.total_seconds() in 2.7.0", +) +@pytest.mark.parametrize("epoch", [1552211999999999872, 1552211999999999999]) +def test_constructor_before_dst_switch(epoch): + # GH 31043 + # Make sure that calling Timestamp constructor + # on time just before DST switch doesn't lead to + # nonexistent time or value change + # Works only with dateutil >= 2.7.0 as dateutil overrid + # pandas.Timedelta.total_seconds with + # datetime.timedelta.total_seconds before + ts = Timestamp(epoch, tz="dateutil/US/Pacific") + result = ts.tz.dst(ts) + expected = timedelta(seconds=0) + assert Timestamp(ts).value == epoch + assert result == expected