39
39
ScalarIndexer ,
40
40
SequenceIndexer ,
41
41
SortKind ,
42
+ TimeArrayLike ,
42
43
npt ,
43
44
)
44
45
from pandas .compat .numpy import function as nv
82
83
ExtensionArray ,
83
84
_extension_array_shared_docs ,
84
85
)
86
+ from pandas .core .arrays .datetimes import DatetimeArray
87
+ from pandas .core .arrays .timedeltas import TimedeltaArray
85
88
import pandas .core .common as com
86
89
from pandas .core .construction import (
87
90
array as pd_array ,
102
105
103
106
104
107
IntervalArrayT = TypeVar ("IntervalArrayT" , bound = "IntervalArray" )
108
+ IntervalSideT = Union [TimeArrayLike , np .ndarray ]
105
109
IntervalOrNA = Union [Interval , float ]
106
110
107
111
_interval_shared_docs : dict [str , str ] = {}
123
127
Parameters
124
128
----------
125
129
data : array-like (1-dimensional)
126
- Array-like containing Interval objects from which to build the
127
- %(klass)s.
130
+ Array-like (ndarray, :class:`DateTimeArray`, :class:`TimeDeltaArray`) containing
131
+ Interval objects from which to build the %(klass)s.
128
132
closed : {'left', 'right', 'both', 'neither'}, default 'right'
129
133
Whether the intervals are closed on the left-side, right-side, both or
130
134
neither.
@@ -213,8 +217,8 @@ def ndim(self) -> Literal[1]:
213
217
return 1
214
218
215
219
# To make mypy recognize the fields
216
- _left : np . ndarray
217
- _right : np . ndarray
220
+ _left : IntervalSideT
221
+ _right : IntervalSideT
218
222
_dtype : IntervalDtype
219
223
220
224
# ---------------------------------------------------------------------
@@ -232,9 +236,10 @@ def __new__(
232
236
data = extract_array (data , extract_numpy = True )
233
237
234
238
if isinstance (data , cls ):
235
- left = data ._left
236
- right = data ._right
239
+ left : IntervalSideT = data ._left
240
+ right : IntervalSideT = data ._right
237
241
closed = closed or data .closed
242
+ dtype = IntervalDtype (left .dtype , closed = closed )
238
243
else :
239
244
240
245
# don't allow scalars
@@ -255,37 +260,57 @@ def __new__(
255
260
right = lib .maybe_convert_objects (right )
256
261
closed = closed or infer_closed
257
262
263
+ left , right , dtype = cls ._ensure_simple_new_inputs (
264
+ left ,
265
+ right ,
266
+ closed = closed ,
267
+ copy = copy ,
268
+ dtype = dtype ,
269
+ )
270
+
271
+ if verify_integrity :
272
+ cls ._validate (left , right , dtype = dtype )
273
+
258
274
return cls ._simple_new (
259
275
left ,
260
276
right ,
261
- closed ,
262
- copy = copy ,
263
277
dtype = dtype ,
264
- verify_integrity = verify_integrity ,
265
278
)
266
279
267
280
@classmethod
268
281
def _simple_new (
269
282
cls : type [IntervalArrayT ],
283
+ left : IntervalSideT ,
284
+ right : IntervalSideT ,
285
+ dtype : IntervalDtype ,
286
+ ) -> IntervalArrayT :
287
+ result = IntervalMixin .__new__ (cls )
288
+ result ._left = left
289
+ result ._right = right
290
+ result ._dtype = dtype
291
+
292
+ return result
293
+
294
+ @classmethod
295
+ def _ensure_simple_new_inputs (
296
+ cls ,
270
297
left ,
271
298
right ,
272
299
closed : IntervalClosedType | None = None ,
273
300
copy : bool = False ,
274
301
dtype : Dtype | None = None ,
275
- verify_integrity : bool = True ,
276
- ) -> IntervalArrayT :
277
- result = IntervalMixin .__new__ (cls )
302
+ ) -> tuple [IntervalSideT , IntervalSideT , IntervalDtype ]:
303
+ """Ensure correctness of input parameters for cls._simple_new."""
304
+ from pandas .core .indexes .base import ensure_index
305
+
306
+ left = ensure_index (left , copy = copy )
307
+ right = ensure_index (right , copy = copy )
278
308
279
309
if closed is None and isinstance (dtype , IntervalDtype ):
280
310
closed = dtype .closed
281
311
282
312
closed = closed or "right"
283
313
284
- from pandas .core .indexes .base import ensure_index
285
-
286
- left = ensure_index (left , copy = copy )
287
- right = ensure_index (right , copy = copy )
288
-
289
314
if dtype is not None :
290
315
# GH 19262: dtype must be an IntervalDtype to override inferred
291
316
dtype = pandas_dtype (dtype )
@@ -346,13 +371,8 @@ def _simple_new(
346
371
right = right .copy ()
347
372
348
373
dtype = IntervalDtype (left .dtype , closed = closed )
349
- result ._dtype = dtype
350
374
351
- result ._left = left
352
- result ._right = right
353
- if verify_integrity :
354
- result ._validate ()
355
- return result
375
+ return left , right , dtype
356
376
357
377
@classmethod
358
378
def _from_sequence (
@@ -512,9 +532,16 @@ def from_arrays(
512
532
left = _maybe_convert_platform_interval (left )
513
533
right = _maybe_convert_platform_interval (right )
514
534
515
- return cls ._simple_new (
516
- left , right , closed , copy = copy , dtype = dtype , verify_integrity = True
535
+ left , right , dtype = cls ._ensure_simple_new_inputs (
536
+ left ,
537
+ right ,
538
+ closed = closed ,
539
+ copy = copy ,
540
+ dtype = dtype ,
517
541
)
542
+ cls ._validate (left , right , dtype = dtype )
543
+
544
+ return cls ._simple_new (left , right , dtype = dtype )
518
545
519
546
_interval_shared_docs ["from_tuples" ] = textwrap .dedent (
520
547
"""
@@ -599,32 +626,33 @@ def from_tuples(
599
626
600
627
return cls .from_arrays (left , right , closed , copy = False , dtype = dtype )
601
628
602
- def _validate (self ):
629
+ @classmethod
630
+ def _validate (cls , left , right , dtype : IntervalDtype ) -> None :
603
631
"""
604
632
Verify that the IntervalArray is valid.
605
633
606
634
Checks that
607
635
608
- * closed is valid
636
+ * dtype is correct
609
637
* left and right match lengths
610
638
* left and right have the same missing values
611
639
* left is always below right
612
640
"""
613
- if self . closed not in VALID_CLOSED :
614
- msg = f"invalid option for 'closed' : { self . closed } "
641
+ if not isinstance ( dtype , IntervalDtype ) :
642
+ msg = f"invalid dtype : { dtype } "
615
643
raise ValueError (msg )
616
- if len (self . _left ) != len (self . _right ):
644
+ if len (left ) != len (right ):
617
645
msg = "left and right must have the same length"
618
646
raise ValueError (msg )
619
- left_mask = notna (self . _left )
620
- right_mask = notna (self . _right )
647
+ left_mask = notna (left )
648
+ right_mask = notna (right )
621
649
if not (left_mask == right_mask ).all ():
622
650
msg = (
623
651
"missing values must be missing in the same "
624
652
"location both left and right sides"
625
653
)
626
654
raise ValueError (msg )
627
- if not (self . _left [left_mask ] <= self . _right [left_mask ]).all ():
655
+ if not (left [left_mask ] <= right [left_mask ]).all ():
628
656
msg = "left side of interval must be <= right side"
629
657
raise ValueError (msg )
630
658
@@ -639,7 +667,11 @@ def _shallow_copy(self: IntervalArrayT, left, right) -> IntervalArrayT:
639
667
right : Index
640
668
Values to be used for the right-side of the intervals.
641
669
"""
642
- return self ._simple_new (left , right , closed = self .closed , verify_integrity = False )
670
+ dtype = IntervalDtype (left .dtype , closed = self .closed )
671
+ left , right , dtype = self ._ensure_simple_new_inputs (left , right , dtype = dtype )
672
+ self ._validate (left , right , dtype = dtype )
673
+
674
+ return self ._simple_new (left , right , dtype = dtype )
643
675
644
676
# ---------------------------------------------------------------------
645
677
# Descriptive
@@ -988,7 +1020,10 @@ def _concat_same_type(
988
1020
989
1021
left = np .concatenate ([interval .left for interval in to_concat ])
990
1022
right = np .concatenate ([interval .right for interval in to_concat ])
991
- return cls ._simple_new (left , right , closed = closed , copy = False )
1023
+
1024
+ left , right , dtype = cls ._ensure_simple_new_inputs (left , right , closed = closed )
1025
+
1026
+ return cls ._simple_new (left , right , dtype = dtype )
992
1027
993
1028
def copy (self : IntervalArrayT ) -> IntervalArrayT :
994
1029
"""
@@ -1000,9 +1035,8 @@ def copy(self: IntervalArrayT) -> IntervalArrayT:
1000
1035
"""
1001
1036
left = self ._left .copy ()
1002
1037
right = self ._right .copy ()
1003
- closed = self .closed
1004
- # TODO: Could skip verify_integrity here.
1005
- return type (self ).from_arrays (left , right , closed = closed )
1038
+ dtype = self .dtype
1039
+ return self ._simple_new (left , right , dtype = dtype )
1006
1040
1007
1041
def isna (self ) -> np .ndarray :
1008
1042
return isna (self ._left )
@@ -1402,9 +1436,9 @@ def set_closed(self: IntervalArrayT, closed: IntervalClosedType) -> IntervalArra
1402
1436
msg = f"invalid option for 'closed': { closed } "
1403
1437
raise ValueError (msg )
1404
1438
1405
- return type ( self ). _simple_new (
1406
- left = self . _left , right = self . _right , closed = closed , verify_integrity = False
1407
- )
1439
+ left , right = self . _left , self . _right
1440
+ dtype = IntervalDtype ( left . dtype , closed = closed )
1441
+ return self . _simple_new ( left , right , dtype = dtype )
1408
1442
1409
1443
_interval_shared_docs [
1410
1444
"is_non_overlapping_monotonic"
@@ -1546,9 +1580,11 @@ def _putmask(self, mask: npt.NDArray[np.bool_], value) -> None:
1546
1580
1547
1581
if isinstance (self ._left , np .ndarray ):
1548
1582
np .putmask (self ._left , mask , value_left )
1583
+ assert isinstance (self ._right , np .ndarray )
1549
1584
np .putmask (self ._right , mask , value_right )
1550
1585
else :
1551
1586
self ._left ._putmask (mask , value_left )
1587
+ assert not isinstance (self ._right , np .ndarray )
1552
1588
self ._right ._putmask (mask , value_right )
1553
1589
1554
1590
def insert (self : IntervalArrayT , loc : int , item : Interval ) -> IntervalArrayT :
@@ -1576,9 +1612,11 @@ def insert(self: IntervalArrayT, loc: int, item: Interval) -> IntervalArrayT:
1576
1612
def delete (self : IntervalArrayT , loc ) -> IntervalArrayT :
1577
1613
if isinstance (self ._left , np .ndarray ):
1578
1614
new_left = np .delete (self ._left , loc )
1615
+ assert isinstance (self ._right , np .ndarray )
1579
1616
new_right = np .delete (self ._right , loc )
1580
1617
else :
1581
1618
new_left = self ._left .delete (loc )
1619
+ assert not isinstance (self ._right , np .ndarray )
1582
1620
new_right = self ._right .delete (loc )
1583
1621
return self ._shallow_copy (left = new_left , right = new_right )
1584
1622
@@ -1679,7 +1717,7 @@ def isin(self, values) -> npt.NDArray[np.bool_]:
1679
1717
return isin (self .astype (object ), values .astype (object ))
1680
1718
1681
1719
@property
1682
- def _combined (self ) -> ArrayLike :
1720
+ def _combined (self ) -> IntervalSideT :
1683
1721
left = self .left ._values .reshape (- 1 , 1 )
1684
1722
right = self .right ._values .reshape (- 1 , 1 )
1685
1723
if needs_i8_conversion (left .dtype ):
@@ -1696,15 +1734,12 @@ def _from_combined(self, combined: np.ndarray) -> IntervalArray:
1696
1734
1697
1735
dtype = self ._left .dtype
1698
1736
if needs_i8_conversion (dtype ):
1699
- # error: "Type[ndarray[Any, Any]]" has no attribute "_from_sequence"
1700
- new_left = type (self ._left )._from_sequence ( # type: ignore[attr-defined]
1701
- nc [:, 0 ], dtype = dtype
1702
- )
1703
- # error: "Type[ndarray[Any, Any]]" has no attribute "_from_sequence"
1704
- new_right = type (self ._right )._from_sequence ( # type: ignore[attr-defined]
1705
- nc [:, 1 ], dtype = dtype
1706
- )
1737
+ assert isinstance (self ._left , (DatetimeArray , TimedeltaArray ))
1738
+ new_left = type (self ._left )._from_sequence (nc [:, 0 ], dtype = dtype )
1739
+ assert isinstance (self ._right , (DatetimeArray , TimedeltaArray ))
1740
+ new_right = type (self ._right )._from_sequence (nc [:, 1 ], dtype = dtype )
1707
1741
else :
1742
+ assert isinstance (dtype , np .dtype )
1708
1743
new_left = nc [:, 0 ].view (dtype )
1709
1744
new_right = nc [:, 1 ].view (dtype )
1710
1745
return self ._shallow_copy (left = new_left , right = new_right )
0 commit comments