diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index b37e5dc620260..6b0dbedbbdfc6 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -567,7 +567,13 @@ cdef class _Timedelta(timedelta): return PyObject_RichCompare(np.array([self]), other, op) return PyObject_RichCompare(other, self, reverse_ops[op]) else: - if op == Py_EQ: + if (getattr(other, "_typ", "") == "dateoffset" and + hasattr(other, "delta")): + # offsets.Tick; we catch this fairly late as it is a + # relatively infrequent case + ots = other.delta + return cmp_scalar(self.value, ots.value, op) + elif op == Py_EQ: return False elif op == Py_NE: return True diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index c260700c9473b..a68d3af889e7b 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -9,9 +9,24 @@ from pandas.core.tools.timedeltas import _coerce_scalar_to_timedelta_type as ct from pandas import (Timedelta, TimedeltaIndex, timedelta_range, Series, to_timedelta, compat) +from pandas.tseries.frequencies import to_offset from pandas._libs.tslib import iNaT, NaT +class TestTimedeltaComparisons(object): + @pytest.mark.parametrize('freq', ['D', 'H', 'T', 's', 'ms', 'us', 'ns']) + def test_tick_comparison(self, freq): + offset = to_offset(freq) * 2 + delta = offset._inc + assert isinstance(delta, Timedelta) + assert delta < offset + assert delta <= offset + assert not delta == offset + assert delta != offset + assert not delta > offset + assert not delta >= offset + + class TestTimedeltaArithmetic(object): _multiprocess_can_split_ = True diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 5b4c2f9d86674..bd2f51ff875e1 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -3147,3 +3147,52 @@ def test_require_integers(offset_types): cls = offset_types with pytest.raises(ValueError): cls(n=1.5) + + +def test_comparisons(offset_types): + cls = offset_types + + if cls is WeekOfMonth: + # TODO: The default values for week and weekday should be non-raising + off = cls(n=1, week=1, weekday=2) + elif cls is LastWeekOfMonth: + # TODO: The default value for weekday should be non-raising + off = cls(n=1, weekday=4) + else: + off = cls(n=1) + + if cls is Week: + assert off < timedelta(days=8) + assert off > timedelta(days=6) + assert off <= Day(n=7) + elif issubclass(cls, offsets.Tick): + pass + else: + with pytest.raises(TypeError): + off < timedelta(days=8) + with pytest.raises(TypeError): + off > timedelta(days=6) + with pytest.raises(TypeError): + off <= Day(n=7) + with pytest.raises(TypeError): + off < DateOffset(month=7) + + +def test_week_comparison(): + # Only Week with weekday == None is special + off = Week(weekday=3) + with pytest.raises(TypeError): + off < timedelta(days=8) + with pytest.raises(TypeError): + off > timedelta(days=6) + with pytest.raises(TypeError): + off <= Day(n=7) + + +@pytest.mark.parametrize('opname', ['__eq__', '__ne__', + '__lt__', '__le__', + '__gt__', '__ge__']) +def test_comparison_names(offset_types, opname): + cls = offset_types + method = getattr(cls, opname) + assert method.__name__ == opname diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 24033d4ff6cbd..d82062c96408b 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -9,7 +9,8 @@ from pandas import Timedelta, Timestamp from pandas.tseries import offsets -from pandas.tseries.offsets import Hour, Minute, Second, Milli, Micro, Nano +from pandas.tseries.offsets import (Hour, Minute, Second, Milli, Micro, Nano, + Week) from .common import assert_offset_equal @@ -35,6 +36,24 @@ def test_delta_to_tick(): assert (tick == offsets.Day(3)) +@pytest.mark.parametrize('cls', tick_classes) +def test_tick_comparisons(cls): + off = cls(n=2) + with pytest.raises(TypeError): + off < 3 + + # Unfortunately there is no good way to make the reverse inequality work + assert off > timedelta(-1) + assert off >= timedelta(-1) + assert off < off._inc * 3 # Timedelta object + assert off <= off._inc * 3 # Timedelta object + assert off == off.delta + assert off.delta == off + assert off != -1 * off + + assert off < Week() + + # --------------------------------------------------------------------- diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 8b12b2f3ad2ce..2937279bdf995 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -112,6 +112,24 @@ def wrapper(self, other): return wrapper +def _make_cmp_func(op): + assert op not in [operator.eq, operator.ne] + # __eq__ and __ne__ have slightly different behavior, returning + # False and True, respectively, instead of raising + + def cmp_func(self, other): + if type(self) == Week and self.weekday is None: + # Week without weekday behaves like a Tick + tick = Day(n=7 * self.n, normalize=self.normalize) + return op(tick, other) + else: + raise TypeError('Cannot compare type {self} with {other}' + .format(self=self.__class__.__name__, + other=other.__class__.__name__)) + + cmp_func.__name__ = '__{name}__'.format(name=op.__name__) + return cmp_func + # --------------------------------------------------------------------- # DateOffset @@ -299,6 +317,11 @@ def _repr_attrs(self): def name(self): return self.rule_code + __lt__ = _make_cmp_func(operator.lt) + __le__ = _make_cmp_func(operator.le) + __ge__ = _make_cmp_func(operator.ge) + __gt__ = _make_cmp_func(operator.gt) + def __eq__(self, other): if other is None: return False @@ -320,9 +343,7 @@ def __hash__(self): return hash(self._params()) def __add__(self, other): - if isinstance(other, (ABCDatetimeIndex, ABCSeries)): - return other + self - elif isinstance(other, ABCPeriod): + if isinstance(other, (ABCDatetimeIndex, ABCSeries, ABCPeriod)): return other + self try: return self.apply(other) @@ -2146,8 +2167,41 @@ def onOffset(self, dt): def _tick_comp(op): def f(self, other): - return op(self.delta, other.delta) + if isinstance(other, Tick): + # Note we cannot just try/except other.delta because Tick.delta + # returns a Timedelta while Timedelta.delta returns an int + other_delta = other.delta + elif isinstance(other, (timedelta, np.timedelta64)): + other_delta = other + elif isinstance(other, Week) and other.weekday is None: + other_delta = timedelta(weeks=other.n) + elif isinstance(other, compat.string_types): + from pandas.tseries.frequencies import to_offset + other = to_offset(other) + if isinstance(other, DateOffset): + return f(self, other) + else: + if op == operator.eq: + return False + elif op == operator.ne: + return True + raise TypeError('Cannot compare type {self} and {other}' + .format(self=self.__class__.__name__, + other=other.__class__.__name__)) + elif op == operator.eq: + # TODO: Consider changing this older behavior for + # __eq__ and __ne__to match other comparisons + return False + elif op == operator.ne: + return True + else: + raise TypeError('Cannot compare type {self} and {other}' + .format(self=self.__class__.__name__, + other=other.__class__.__name__)) + + return op(self.delta, other_delta) + f.__name__ = '__{name}__'.format(name=op.__name__) return f @@ -2184,34 +2238,11 @@ def __add__(self, other): raise OverflowError("the add operation between {self} and {other} " "will overflow".format(self=self, other=other)) - def __eq__(self, other): - if isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if isinstance(other, Tick): - return self.delta == other.delta - else: - # TODO: Are there cases where this should raise TypeError? - return False - - # This is identical to DateOffset.__hash__, but has to be redefined here - # for Python 3, because we've redefined __eq__. def __hash__(self): - return hash(self._params()) - - def __ne__(self, other): - if isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if isinstance(other, Tick): - return self.delta != other.delta - else: - # TODO: Are there cases where this should raise TypeError? - return True + # This is identical to DateOffset.__hash__, but has to be redefined + # here for Python 3, because we've redefined __eq__. + tup = (str(self.__class__), ('n', self.n)) + return hash(tup) @property def delta(self):