diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 68c1839221508..43c75cde74b42 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -24,6 +24,41 @@ Other Enhancements Backwards incompatible API changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _whatsnew_0240.api.datetimelike.normalize + +Tick DateOffset Normalize Restrictions +-------------------------------------- + +Creating a ``Tick`` object (:class:``Day``, :class:``Hour``, :class:``Minute``, +:class:``Second``, :class:``Milli``, :class:``Micro``, :class:``Nano``) with +`normalize=True` is no longer supported. This prevents unexpected behavior +where addition could fail to be monotone or associative. (:issue:`21427`) + +.. ipython:: python + + ts = pd.Timestamp('2018-06-11 18:01:14') + ts + tic = pd.offsets.Hour(n=2, normalize=True) + tic + +Previous Behavior: + +.. code-block:: ipython + + In [4]: ts + tic + Out [4]: Timestamp('2018-06-11 00:00:00') + + In [5]: ts + tic + tic + tic == ts + (tic + tic + tic) + Out [5]: False + +Current Behavior: + +.. ipython:: python + + tic = pd.offsets.Hour(n=2) + ts + tic + tic + tic == ts + (tic + tic + tic) + + .. _whatsnew_0240.api.datetimelike: Datetimelike API Changes diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 8bf0d9f915d04..33e5a70c4c30b 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -28,7 +28,7 @@ YearEnd, Day, QuarterEnd, BusinessMonthEnd, FY5253, Nano, Easter, FY5253Quarter, - LastWeekOfMonth) + LastWeekOfMonth, Tick) from pandas.core.tools.datetimes import format, ole2datetime import pandas.tseries.offsets as offsets from pandas.io.pickle import read_pickle @@ -270,6 +270,11 @@ def test_offset_freqstr(self, offset_types): def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=False): + + if normalize and issubclass(offset, Tick): + # normalize=True disallowed for Tick subclasses GH#21427 + return + offset_s = self._get_offset(offset, normalize=normalize) func = getattr(offset_s, funcname) @@ -458,6 +463,9 @@ def test_onOffset(self, offset_types): assert offset_s.onOffset(dt) # when normalize=True, onOffset checks time is 00:00:00 + if issubclass(offset_types, Tick): + # normalize=True disallowed for Tick subclasses GH#21427 + return offset_n = self._get_offset(offset_types, normalize=True) assert not offset_n.onOffset(dt) @@ -485,7 +493,9 @@ def test_add(self, offset_types, tz): assert isinstance(result, Timestamp) assert result == expected_localize - # normalize=True + # normalize=True, disallowed for Tick subclasses GH#21427 + if issubclass(offset_types, Tick): + return offset_s = self._get_offset(offset_types, normalize=True) expected = Timestamp(expected.date()) @@ -3098,6 +3108,14 @@ def test_require_integers(offset_types): cls(n=1.5) +def test_tick_normalize_raises(tick_classes): + # check that trying to create a Tick object with normalize=True raises + # GH#21427 + cls = tick_classes + with pytest.raises(ValueError): + cls(n=3, normalize=True) + + def test_weeks_onoffset(): # GH#18510 Week with weekday = None, normalize = False should always # be onOffset diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index a5a983bf94bb8..ecd15bc7b04b8 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2217,8 +2217,10 @@ class Tick(SingleConstructorOffset): _attributes = frozenset(['n', 'normalize']) def __init__(self, n=1, normalize=False): - # TODO: do Tick classes with normalize=True make sense? self.n = self._validate_n(n) + if normalize: + raise ValueError("Tick offset with `normalize=True` are not " + "allowed.") # GH#21427 self.normalize = normalize __gt__ = _tick_comp(operator.gt)