diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index f8bcbcfb158b5..10becdce5d6dd 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -179,7 +179,8 @@ cdef class IntervalMixin: return (self.right == self.left) & (self.closed != 'both') def _check_closed_matches(self, other, name='other'): - """Check if the closed attribute of `other` matches. + """ + Check if the closed attribute of `other` matches. Note that 'left' and 'right' are considered different from 'both'. diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 267d1330faceb..21264b00b91f8 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -130,19 +130,13 @@ def wrapped(self, other, sort=False): if op_name in ("difference",): result = result.astype(self.dtype) return result - elif self.closed != other.closed: - raise ValueError( - "can only do set operations between two IntervalIndex " - "objects that are closed on the same side" - ) - # GH 19016: ensure set op will not return a prohibited dtype - subtypes = [self.dtype.subtype, other.dtype.subtype] - common_subtype = find_common_type(subtypes) - if is_object_dtype(common_subtype): + if self._is_non_comparable_own_type(other): + # GH#19016: ensure set op will not return a prohibited dtype raise TypeError( - f"can only do {op_name} between two IntervalIndex " - "objects that have compatible dtypes" + "can only do set operations between two IntervalIndex " + "objects that are closed on the same side " + "and have compatible dtypes" ) return method(self, other, sort) @@ -717,11 +711,8 @@ def get_indexer( if self.equals(target_as_index): return np.arange(len(self), dtype="intp") - # different closed or incompatible subtype -> no matches - common_subtype = find_common_type( - [self.dtype.subtype, target_as_index.dtype.subtype] - ) - if self.closed != target_as_index.closed or is_object_dtype(common_subtype): + if self._is_non_comparable_own_type(target_as_index): + # different closed or incompatible subtype -> no matches return np.repeat(np.intp(-1), len(target_as_index)) # non-overlapping -> at most one match per interval in target_as_index @@ -763,10 +754,8 @@ def get_indexer_non_unique( # check that target_as_index IntervalIndex is compatible if isinstance(target_as_index, IntervalIndex): - common_subtype = find_common_type( - [self.dtype.subtype, target_as_index.dtype.subtype] - ) - if self.closed != target_as_index.closed or is_object_dtype(common_subtype): + + if self._is_non_comparable_own_type(target_as_index): # different closed or incompatible subtype -> no matches return ( np.repeat(-1, len(target_as_index)), @@ -837,6 +826,16 @@ def _convert_list_indexer(self, keyarr): return locs + def _is_non_comparable_own_type(self, other: "IntervalIndex") -> bool: + # different closed or incompatible subtype -> no matches + + # TODO: once closed is part of IntervalDtype, we can just define + # is_comparable_dtype GH#19371 + if self.closed != other.closed: + return True + common_subtype = find_common_type([self.dtype.subtype, other.dtype.subtype]) + return is_object_dtype(common_subtype) + # -------------------------------------------------------------------- @cache_readonly diff --git a/pandas/tests/indexes/interval/test_setops.py b/pandas/tests/indexes/interval/test_setops.py index 562497b29af12..0b94d70367b4d 100644 --- a/pandas/tests/indexes/interval/test_setops.py +++ b/pandas/tests/indexes/interval/test_setops.py @@ -159,18 +159,18 @@ def test_set_incompatible_types(self, closed, op_name, sort): # mixed closed msg = ( "can only do set operations between two IntervalIndex objects " - "that are closed on the same side" + "that are closed on the same side and have compatible dtypes" ) for other_closed in {"right", "left", "both", "neither"} - {closed}: other = monotonic_index(0, 11, closed=other_closed) - with pytest.raises(ValueError, match=msg): + with pytest.raises(TypeError, match=msg): set_op(other, sort=sort) # GH 19016: incompatible dtypes other = interval_range(Timestamp("20180101"), periods=9, closed=closed) msg = ( - f"can only do {op_name} between two IntervalIndex objects that have " - "compatible dtypes" + "can only do set operations between two IntervalIndex objects " + "that are closed on the same side and have compatible dtypes" ) with pytest.raises(TypeError, match=msg): set_op(other, sort=sort)