Skip to content

Commit f3b6eb1

Browse files
committed
ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613)
1 parent a62897a commit f3b6eb1

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
@@ -279,6 +279,7 @@ Other enhancements
279279
- :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`)
280280
- :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`)
281281
- :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`)
282+
- :class:`Interval` now supports checking whether one interval is inside of another interval (:issue:`46613`)
282283

283284
.. ---------------------------------------------------------------------------
284285
.. _whatsnew_150.notable_bug_fixes:

pandas/_libs/interval.pyi

+9-2
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,17 @@ class Interval(IntervalMixin, Generic[_OrderableT]):
8181
def __hash__(self) -> int: ...
8282
@overload
8383
def __contains__(
84-
self: Interval[_OrderableTimesT], key: _OrderableTimesT
84+
self: Interval[Timedelta], key: Timedelta | Interval[Timedelta]
8585
) -> bool: ...
8686
@overload
87-
def __contains__(self: Interval[_OrderableScalarT], key: int | float) -> bool: ...
87+
def __contains__(
88+
self: Interval[Timestamp], key: Timestamp | Interval[Timestamp]
89+
) -> bool: ...
90+
@overload
91+
def __contains__(
92+
self: Interval[_OrderableScalarT],
93+
key: _OrderableScalarT | Interval[_OrderableScalarT],
94+
) -> bool: ...
8895
@overload
8996
def __add__(
9097
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
@@ -407,10 +409,12 @@ cdef class Interval(IntervalMixin):
407409
return hash((self.left, self.right, self.inclusive))
408410

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

415419
def __richcmp__(self, other, op: int):
416420
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)