Skip to content

Commit 98764bd

Browse files
committed
ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613)
1 parent 8b5dfa2 commit 98764bd

File tree

5 files changed

+74
-11
lines changed

5 files changed

+74
-11
lines changed

doc/source/whatsnew/v1.5.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ Other enhancements
282282
- :class:`Series` reducers (e.g. ``min``, ``max``, ``sum``, ``mean``) will now successfully operate when the dtype is numeric and ``numeric_only=True`` is provided; previously this would raise a ``NotImplementedError`` (:issue:`47500`)
283283
- :meth:`RangeIndex.union` now can return a :class:`RangeIndex` instead of a :class:`Int64Index` if the resulting values are equally spaced (:issue:`47557`, :issue:`43885`)
284284
- :meth:`DataFrame.compare` now accepts an argument ``result_names`` to allow the user to specify the result's names of both left and right DataFrame which are being compared. This is by default ``'self'`` and ``'other'`` (:issue:`44354`)
285+
- :class:`Interval` now supports checking whether one interval is inside of another interval (:issue:`46613`)
285286

286287
.. ---------------------------------------------------------------------------
287288
.. _whatsnew_150.notable_bug_fixes:

pandas/_libs/interval.pyi

+9-2
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,17 @@ class Interval(IntervalMixin, Generic[_OrderableT]):
7979
def __hash__(self) -> int: ...
8080
@overload
8181
def __contains__(
82-
self: Interval[_OrderableTimesT], key: _OrderableTimesT
82+
self: Interval[Timedelta], key: Timedelta | Interval[Timedelta]
8383
) -> bool: ...
8484
@overload
85-
def __contains__(self: Interval[_OrderableScalarT], key: float) -> bool: ...
85+
def __contains__(
86+
self: Interval[Timestamp], key: Timestamp | Interval[Timestamp]
87+
) -> bool: ...
88+
@overload
89+
def __contains__(
90+
self: Interval[_OrderableScalarT],
91+
key: _OrderableScalarT | Interval[_OrderableScalarT],
92+
) -> bool: ...
8693
@overload
8794
def __add__(
8895
self: Interval[_OrderableTimesT], y: Timedelta

pandas/_libs/interval.pyx

+9-5
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,12 @@ cdef class Interval(IntervalMixin):
297297
>>> iv
298298
Interval(0, 5, inclusive='right')
299299
300-
You can check if an element belongs to it
300+
You can check if an element belongs to it, or if it contains another interval:
301301
302302
>>> 2.5 in iv
303303
True
304+
>>> pd.Interval(left=2, right=5, inclusive='both') in iv
305+
True
304306
305307
You can test the bounds (``inclusive='right'``, so ``0 < x <= 5``):
306308
@@ -409,10 +411,12 @@ cdef class Interval(IntervalMixin):
409411
return hash((self.left, self.right, self.inclusive))
410412

411413
def __contains__(self, key) -> bool:
412-
if _interval_like(key):
413-
raise TypeError("__contains__ not defined for two intervals")
414-
return ((self.left < key if self.open_left else self.left <= key) and
415-
(key < self.right if self.open_right else key <= self.right))
414+
if isinstance(key, Interval):
415+
return ((self.left < key.left if self.open_left and key.closed_left else self.left <= key.left) and
416+
(key.right < self.right if self.open_right and key.closed_right else key.right <= self.right))
417+
elif isinstance(key, _Timestamp) or is_timedelta64_object(key) or is_float_object(key) or is_integer_object(key):
418+
return ((self.left < key if self.open_left else self.left <= key) and
419+
(key < self.right if self.open_right else key <= self.right))
416420

417421
def __richcmp__(self, other, op: int):
418422
if isinstance(other, Interval):

pandas/tests/scalar/interval/test_interval.py

-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,6 @@ def test_contains(self, interval):
3636
assert 1 in interval
3737
assert 0 not in interval
3838

39-
msg = "__contains__ not defined for two intervals"
40-
with pytest.raises(TypeError, match=msg):
41-
interval in interval
42-
4339
interval_both = Interval(0, 1, "both")
4440
assert 0 in interval_both
4541
assert 1 in interval_both

pandas/tests/scalar/interval/test_ops.py

+55
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,58 @@ def test_overlaps_invalid_type(self, other):
6666
msg = f"`other` must be an Interval, got {type(other).__name__}"
6767
with pytest.raises(TypeError, match=msg):
6868
interval.overlaps(other)
69+
70+
71+
class TestContains:
72+
@pytest.mark.parametrize(
73+
"closed",
74+
["neither", "left", "right"],
75+
)
76+
def test_contains_interval(self, closed):
77+
interval1 = Interval(0, 1, "both")
78+
interval2 = Interval(0, 1, closed)
79+
assert interval1 in interval1
80+
assert interval2 in interval2
81+
assert interval2 in interval1
82+
assert interval1 not in interval2
83+
84+
def test_contains_infinite_length(self):
85+
interval1 = Interval(0, 1, "both")
86+
interval2 = Interval(float("-inf"), float("inf"), "neither")
87+
assert interval1 in interval2
88+
assert interval2 not in interval1
89+
90+
def test_contains_zero_length(self):
91+
interval1 = Interval(0, 1, "both")
92+
interval2 = Interval(-1, -1, "both")
93+
interval3 = Interval(0.5, 0.5, "both")
94+
assert interval2 not in interval1
95+
assert interval3 in interval1
96+
assert interval2 not in interval3 and interval3 not in interval2
97+
assert interval1 not in interval2 and interval1 not in interval3
98+
99+
@pytest.mark.parametrize(
100+
"type1",
101+
[
102+
(0, 1),
103+
(Timestamp(2000, 1, 1, 0), Timestamp(2000, 1, 1, 1)),
104+
(Timedelta("0h"), Timedelta("1h")),
105+
],
106+
)
107+
@pytest.mark.parametrize(
108+
"type2",
109+
[
110+
(0, 1),
111+
(Timestamp(2000, 1, 1, 0), Timestamp(2000, 1, 1, 1)),
112+
(Timedelta("0h"), Timedelta("1h")),
113+
],
114+
)
115+
def test_contains_mixed_types(self, type1, type2):
116+
interval1 = Interval(*type1)
117+
interval2 = Interval(*type2)
118+
if type1 == type2:
119+
assert interval1 in interval2
120+
else:
121+
msg = "^'<=' not supported between instances of"
122+
with pytest.raises(TypeError, match=msg):
123+
interval1 in interval2

0 commit comments

Comments
 (0)