Repository: pandas-dev/pandas

Issue:
Tick with normalize=True should be disallowed
See #21427 

The issue is that a (non-`Day`) `Tick` object with `normalize=True` breaks addition monotonicity/associativity:

```
now = pd.Timestamp.now()
tick = pd.offsets.Minute(n=3, normalize=True)

>>> now
Timestamp('2018-06-11 17:38:10.135400')
>>> now + tick
Timestamp('2018-06-11 00:00:00')

>>> now + 64*tick + 64*tick
Timestamp('2018-06-11 00:00:00')
>>> now + 128*tick
Timestamp('2018-06-12 00:00:00')
```

The `Day` tick doesn't have the same problem, but I'd prefer to disallow it rather than make it a double-special-case.  (See #20633 for more discussion about how `Day` should behave)


Discussion:

Resolving patch:
diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py
--- 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)


Test patch:
diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py
--- 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