Skip to content

Commit 2a86a3d

Browse files
kapiliyerchoucavalier
authored andcommitted
ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) (pandas-dev#47927)
* ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) * ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) * Update doc/source/whatsnew/v1.5.0.rst Co-authored-by: Valentin Iovene <[email protected]> * ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) * ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) * Fix: Unintentionally Modified Range * ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) * Fix: Unintentionally Modified Range * ENH: Support For Interval __contains__ Other Interval (pandas-dev#46613) Co-authored-by: Valentin Iovene <[email protected]>
1 parent 8312a5e commit 2a86a3d

File tree

5 files changed

+75
-8
lines changed

5 files changed

+75
-8
lines changed

doc/source/whatsnew/v1.5.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ Other enhancements
292292
- :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`)
293293
- :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`)
294294
- :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`)
295+
- :class:`Interval` now supports checking whether one interval is contained by another interval (:issue:`46613`)
295296
- :meth:`Series.add_suffix`, :meth:`DataFrame.add_suffix`, :meth:`Series.add_prefix` and :meth:`DataFrame.add_prefix` support a ``copy`` argument. If ``False``, the underlying data is not copied in the returned object (:issue:`47934`)
296297
- :meth:`DataFrame.set_index` now supports a ``copy`` keyword. If ``False``, the underlying data is not copied when a new :class:`DataFrame` is returned (:issue:`48043`)
297298

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

+14-2
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,12 @@ cdef class Interval(IntervalMixin):
299299
>>> iv
300300
Interval(0, 5, inclusive='right')
301301
302-
You can check if an element belongs to it
302+
You can check if an element belongs to it, or if it contains another interval:
303303
304304
>>> 2.5 in iv
305305
True
306+
>>> pd.Interval(left=2, right=5, inclusive='both') in iv
307+
True
306308
307309
You can test the bounds (``inclusive='right'``, so ``0 < x <= 5``):
308310
@@ -412,7 +414,17 @@ cdef class Interval(IntervalMixin):
412414

413415
def __contains__(self, key) -> bool:
414416
if _interval_like(key):
415-
raise TypeError("__contains__ not defined for two intervals")
417+
key_closed_left = key.inclusive in ('left', 'both')
418+
key_closed_right = key.inclusive in ('right', 'both')
419+
if self.open_left and key_closed_left:
420+
left_contained = self.left < key.left
421+
else:
422+
left_contained = self.left <= key.left
423+
if self.open_right and key_closed_right:
424+
right_contained = key.right < self.right
425+
else:
426+
right_contained = key.right <= self.right
427+
return left_contained and right_contained
416428
return ((self.left < key if self.open_left else self.left <= key) and
417429
(key < self.right if self.open_right else key <= self.right))
418430

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

+51
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,54 @@ 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+
def test_contains_interval(self, inclusive_endpoints_fixture):
73+
interval1 = Interval(0, 1, "both")
74+
interval2 = Interval(0, 1, inclusive_endpoints_fixture)
75+
assert interval1 in interval1
76+
assert interval2 in interval2
77+
assert interval2 in interval1
78+
assert interval1 not in interval2 or inclusive_endpoints_fixture == "both"
79+
80+
def test_contains_infinite_length(self):
81+
interval1 = Interval(0, 1, "both")
82+
interval2 = Interval(float("-inf"), float("inf"), "neither")
83+
assert interval1 in interval2
84+
assert interval2 not in interval1
85+
86+
def test_contains_zero_length(self):
87+
interval1 = Interval(0, 1, "both")
88+
interval2 = Interval(-1, -1, "both")
89+
interval3 = Interval(0.5, 0.5, "both")
90+
assert interval2 not in interval1
91+
assert interval3 in interval1
92+
assert interval2 not in interval3 and interval3 not in interval2
93+
assert interval1 not in interval2 and interval1 not in interval3
94+
95+
@pytest.mark.parametrize(
96+
"type1",
97+
[
98+
(0, 1),
99+
(Timestamp(2000, 1, 1, 0), Timestamp(2000, 1, 1, 1)),
100+
(Timedelta("0h"), Timedelta("1h")),
101+
],
102+
)
103+
@pytest.mark.parametrize(
104+
"type2",
105+
[
106+
(0, 1),
107+
(Timestamp(2000, 1, 1, 0), Timestamp(2000, 1, 1, 1)),
108+
(Timedelta("0h"), Timedelta("1h")),
109+
],
110+
)
111+
def test_contains_mixed_types(self, type1, type2):
112+
interval1 = Interval(*type1)
113+
interval2 = Interval(*type2)
114+
if type1 == type2:
115+
assert interval1 in interval2
116+
else:
117+
msg = "^'<=' not supported between instances of"
118+
with pytest.raises(TypeError, match=msg):
119+
interval1 in interval2

0 commit comments

Comments
 (0)