28
28
Resolution ,
29
29
Tick ,
30
30
parsing ,
31
+ to_offset ,
31
32
)
32
33
from pandas .compat .numpy import function as nv
33
34
from pandas .util ._decorators import (
61
62
NDArrayBackedExtensionIndex ,
62
63
inherit_names ,
63
64
)
65
+ from pandas .core .indexes .range import RangeIndex
64
66
from pandas .core .tools .timedeltas import to_timedelta
65
67
66
68
if TYPE_CHECKING :
@@ -433,12 +435,61 @@ def values(self) -> np.ndarray:
433
435
# --------------------------------------------------------------------
434
436
# Set Operation Methods
435
437
438
+ @cache_readonly
439
+ def _as_range_index (self ) -> RangeIndex :
440
+ # Convert our i8 representations to RangeIndex
441
+ # Caller is responsible for checking isinstance(self.freq, Tick)
442
+ freq = cast (Tick , self .freq )
443
+ tick = freq .delta .value
444
+ rng = range (self [0 ].value , self [- 1 ].value + tick , tick )
445
+ return RangeIndex (rng )
446
+
447
+ def _can_range_setop (self , other ):
448
+ return isinstance (self .freq , Tick ) and isinstance (other .freq , Tick )
449
+
450
+ def _wrap_range_setop (self , other , res_i8 ):
451
+ new_freq = None
452
+ if not len (res_i8 ):
453
+ # RangeIndex defaults to step=1, which we don't want.
454
+ new_freq = self .freq
455
+ elif isinstance (res_i8 , RangeIndex ):
456
+ new_freq = to_offset (Timedelta (res_i8 .step ))
457
+ res_i8 = res_i8
458
+
459
+ # TODO: we cannot just do
460
+ # type(self._data)(res_i8.values, dtype=self.dtype, freq=new_freq)
461
+ # because test_setops_preserve_freq fails with _validate_frequency raising.
462
+ # This raising is incorrect, as 'on_freq' is incorrect. This will
463
+ # be fixed by GH#41493
464
+ res_values = res_i8 .values .view (self ._data ._ndarray .dtype )
465
+ result = type (self ._data )._simple_new (
466
+ res_values , dtype = self .dtype , freq = new_freq
467
+ )
468
+ return self ._wrap_setop_result (other , result )
469
+
470
+ def _range_intersect (self , other , sort ):
471
+ # Dispatch to RangeIndex intersection logic.
472
+ left = self ._as_range_index
473
+ right = other ._as_range_index
474
+ res_i8 = left .intersection (right , sort = sort )
475
+ return self ._wrap_range_setop (other , res_i8 )
476
+
477
+ def _range_union (self , other , sort ):
478
+ # Dispatch to RangeIndex union logic.
479
+ left = self ._as_range_index
480
+ right = other ._as_range_index
481
+ res_i8 = left .union (right , sort = sort )
482
+ return self ._wrap_range_setop (other , res_i8 )
483
+
436
484
def _intersection (self , other : Index , sort = False ) -> Index :
437
485
"""
438
486
intersection specialized to the case with matching dtypes and both non-empty.
439
487
"""
440
488
other = cast ("DatetimeTimedeltaMixin" , other )
441
489
490
+ if self ._can_range_setop (other ):
491
+ return self ._range_intersect (other , sort = sort )
492
+
442
493
if not self ._can_fast_intersect (other ):
443
494
result = Index ._intersection (self , other , sort = sort )
444
495
# We need to invalidate the freq because Index._intersection
@@ -453,7 +504,6 @@ def _intersection(self, other: Index, sort=False) -> Index:
453
504
return self ._fast_intersect (other , sort )
454
505
455
506
def _fast_intersect (self , other , sort ):
456
-
457
507
# to make our life easier, "sort" the two ranges
458
508
if self [0 ] <= other [0 ]:
459
509
left , right = self , other
@@ -485,19 +535,9 @@ def _can_fast_intersect(self: _T, other: _T) -> bool:
485
535
# Because freq is not None, we must then be monotonic decreasing
486
536
return False
487
537
488
- elif self .freq .is_anchored ():
489
- # this along with matching freqs ensure that we "line up",
490
- # so intersection will preserve freq
491
- # GH#42104
492
- return self .freq .n == 1
493
-
494
- elif isinstance (self .freq , Tick ):
495
- # We "line up" if and only if the difference between two of our points
496
- # is a multiple of our freq
497
- diff = self [0 ] - other [0 ]
498
- remainder = diff % self .freq .delta
499
- return remainder == Timedelta (0 )
500
-
538
+ # this along with matching freqs ensure that we "line up",
539
+ # so intersection will preserve freq
540
+ # Note we are assuming away Ticks, as those go through _range_intersect
501
541
# GH#42104
502
542
return self .freq .n == 1
503
543
@@ -516,6 +556,7 @@ def _can_fast_union(self: _T, other: _T) -> bool:
516
556
return False
517
557
518
558
if len (self ) == 0 or len (other ) == 0 :
559
+ # only reached via union_many
519
560
return True
520
561
521
562
# to make our life easier, "sort" the two ranges
@@ -544,10 +585,7 @@ def _fast_union(self: _TDT, other: _TDT, sort=None) -> _TDT:
544
585
loc = right .searchsorted (left_start , side = "left" )
545
586
right_chunk = right ._values [:loc ]
546
587
dates = concat_compat ((left ._values , right_chunk ))
547
- # With sort being False, we can't infer that result.freq == self.freq
548
- # TODO: no tests rely on the _with_freq("infer"); needed?
549
588
result = type (self )._simple_new (dates , name = self .name )
550
- result = result ._with_freq ("infer" )
551
589
return result
552
590
else :
553
591
left , right = other , self
@@ -573,6 +611,9 @@ def _union(self, other, sort):
573
611
assert isinstance (other , type (self ))
574
612
assert self .dtype == other .dtype
575
613
614
+ if self ._can_range_setop (other ):
615
+ return self ._range_union (other , sort = sort )
616
+
576
617
if self ._can_fast_union (other ):
577
618
result = self ._fast_union (other , sort = sort )
578
619
# in the case with sort=None, the _can_fast_union check ensures
0 commit comments