1
1
"""
2
2
Base and utility classes for tseries type pandas objects.
3
3
"""
4
- from datetime import datetime
4
+ from datetime import datetime , timedelta
5
5
from typing import Any , List , Optional , Union , cast
6
6
7
7
import numpy as np
17
17
ensure_int64 ,
18
18
ensure_platform_int ,
19
19
is_bool_dtype ,
20
+ is_datetime64_any_dtype ,
20
21
is_dtype_equal ,
21
22
is_integer ,
22
23
is_list_like ,
24
+ is_object_dtype ,
23
25
is_period_dtype ,
24
26
is_scalar ,
27
+ is_timedelta64_dtype ,
25
28
)
26
29
from pandas .core .dtypes .concat import concat_compat
27
30
from pandas .core .dtypes .generic import ABCIndex , ABCIndexClass , ABCSeries
31
+ from pandas .core .dtypes .missing import isna
28
32
29
33
from pandas .core import algorithms
30
34
from pandas .core .arrays import DatetimeArray , PeriodArray , TimedeltaArray
41
45
from pandas .core .ops import get_op_result_name
42
46
from pandas .core .tools .timedeltas import to_timedelta
43
47
44
- from pandas .tseries .frequencies import DateOffset
48
+ from pandas .tseries .frequencies import DateOffset , to_offset
49
+ from pandas .tseries .offsets import Tick
45
50
46
51
_index_doc_kwargs = dict (ibase ._index_doc_kwargs )
47
52
@@ -72,13 +77,33 @@ def wrapper(left, right):
72
77
return wrapper
73
78
74
79
80
+ def _make_wrapped_arith_op_with_freq (opname : str ):
81
+ """
82
+ Dispatch the operation to the underlying ExtensionArray, and infer
83
+ the appropriate frequency for the result.
84
+ """
85
+ meth = make_wrapped_arith_op (opname )
86
+
87
+ def wrapped (self , other ):
88
+ result = meth (self , other )
89
+ if result is NotImplemented :
90
+ return NotImplemented
91
+
92
+ new_freq = self ._get_addsub_freq (other )
93
+ result ._freq = new_freq
94
+ return result
95
+
96
+ wrapped .__name__ = opname
97
+ return wrapped
98
+
99
+
75
100
@inherit_names (
76
101
["inferred_freq" , "_isnan" , "_resolution" , "resolution" ],
77
102
DatetimeLikeArrayMixin ,
78
103
cache = True ,
79
104
)
80
105
@inherit_names (
81
- ["mean" , "freq" , "freqstr" , " asi8" , "_box_func" ], DatetimeLikeArrayMixin ,
106
+ ["mean" , "asi8" , "_box_func" ], DatetimeLikeArrayMixin ,
82
107
)
83
108
class DatetimeIndexOpsMixin (ExtensionIndex ):
84
109
"""
@@ -446,10 +471,45 @@ def get_indexer_non_unique(self, target):
446
471
return ensure_platform_int (indexer ), missing
447
472
448
473
# --------------------------------------------------------------------
474
+ # Arithmetic Methods
475
+
476
+ def _get_addsub_freq (self , other ) -> Optional [DateOffset ]:
477
+ """
478
+ Find the freq we expect the result of an addition/subtraction operation
479
+ to have.
480
+ """
481
+ if is_period_dtype (self .dtype ):
482
+ # Only used for ops that stay PeriodDtype
483
+ return self .freq
484
+ elif self .freq is None :
485
+ return None
486
+ elif lib .is_scalar (other ) and isna (other ):
487
+ return None
488
+
489
+ elif isinstance (other , (Tick , timedelta , np .timedelta64 )):
490
+ new_freq = None
491
+ if isinstance (self .freq , Tick ):
492
+ new_freq = self .freq
493
+ return new_freq
494
+
495
+ elif isinstance (other , DateOffset ):
496
+ # otherwise just DatetimeArray
497
+ return None # TODO: Should we infer if it matches self.freq * n?
498
+ elif isinstance (other , (datetime , np .datetime64 )):
499
+ return self .freq
500
+
501
+ elif is_timedelta64_dtype (other ):
502
+ return None # TODO: shouldnt we be able to do self.freq + other.freq?
503
+ elif is_object_dtype (other ):
504
+ return None # TODO: is this quite right? sometimes we unpack singletons
505
+ elif is_datetime64_any_dtype (other ):
506
+ return None # TODO: shouldnt we be able to do self.freq + other.freq?
507
+ else :
508
+ raise NotImplementedError
449
509
450
- __add__ = make_wrapped_arith_op ("__add__" )
510
+ __add__ = _make_wrapped_arith_op_with_freq ("__add__" )
511
+ __sub__ = _make_wrapped_arith_op_with_freq ("__sub__" )
451
512
__radd__ = make_wrapped_arith_op ("__radd__" )
452
- __sub__ = make_wrapped_arith_op ("__sub__" )
453
513
__rsub__ = make_wrapped_arith_op ("__rsub__" )
454
514
__pow__ = make_wrapped_arith_op ("__pow__" )
455
515
__rpow__ = make_wrapped_arith_op ("__rpow__" )
@@ -558,7 +618,9 @@ def shift(self, periods=1, freq=None):
558
618
Index.shift : Shift values of Index.
559
619
PeriodIndex.shift : Shift values of PeriodIndex.
560
620
"""
561
- result = self ._data ._time_shift (periods , freq = freq )
621
+ arr = self ._data .view ()
622
+ arr ._freq = self .freq
623
+ result = arr ._time_shift (periods , freq = freq )
562
624
return type (self )(result , name = self .name )
563
625
564
626
# --------------------------------------------------------------------
@@ -610,21 +672,40 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, Int64Index):
610
672
_is_monotonic_increasing = Index .is_monotonic_increasing
611
673
_is_monotonic_decreasing = Index .is_monotonic_decreasing
612
674
_is_unique = Index .is_unique
675
+ _freq = lib .no_default
613
676
614
- def _set_freq (self , freq ):
677
+ @property
678
+ def freq (self ):
679
+ """
680
+ In limited circumstances, our freq may differ from that of our _data.
615
681
"""
616
- Set the _freq attribute on our underlying DatetimeArray.
682
+ if self ._freq is not lib .no_default :
683
+ return self ._freq
684
+ return self ._data .freq
617
685
618
- Parameters
619
- ----------
620
- freq : DateOffset, None, or "infer"
686
+ @property
687
+ def freqstr (self ):
688
+ """
689
+ Return the frequency object as a string if its set, otherwise None.
621
690
"""
622
- # GH#29843
623
- self ._data ._with_freq (freq )
691
+ if self .freq is None :
692
+ return None
693
+ return self .freq .freqstr
624
694
625
695
def _with_freq (self , freq ):
626
696
index = self .copy (deep = False )
627
- index ._set_freq (freq )
697
+ if freq is None :
698
+ # Even if we _can_ have a freq, we might want to set it to None
699
+ index ._freq = None
700
+ elif len (self ) == 0 and isinstance (freq , DateOffset ):
701
+ # Always valid. In the TimedeltaArray case, we assume this
702
+ # is a Tick offset.
703
+ index ._freq = freq
704
+ else :
705
+ assert freq == "infer" , freq
706
+ freq = to_offset (self .inferred_freq )
707
+ index ._freq = freq
708
+
628
709
return index
629
710
630
711
def _shallow_copy (self , values = None , name : Label = lib .no_default ):
@@ -647,8 +728,7 @@ def _shallow_copy(self, values=None, name: Label = lib.no_default):
647
728
648
729
@Appender (Index .difference .__doc__ )
649
730
def difference (self , other , sort = None ):
650
- new_idx = super ().difference (other , sort = sort )
651
- new_idx ._set_freq (None )
731
+ new_idx = super ().difference (other , sort = sort )._with_freq (None )
652
732
return new_idx
653
733
654
734
def intersection (self , other , sort = False ):
@@ -693,7 +773,7 @@ def intersection(self, other, sort=False):
693
773
result = Index .intersection (self , other , sort = sort )
694
774
if isinstance (result , type (self )):
695
775
if result .freq is None :
696
- result . _set_freq ("infer" )
776
+ result = result . _with_freq ("infer" )
697
777
return result
698
778
699
779
elif (
@@ -704,14 +784,7 @@ def intersection(self, other, sort=False):
704
784
or (not self .is_monotonic or not other .is_monotonic )
705
785
):
706
786
result = Index .intersection (self , other , sort = sort )
707
-
708
- # Invalidate the freq of `result`, which may not be correct at
709
- # this point, depending on the values.
710
-
711
- result ._set_freq (None )
712
- result = self ._shallow_copy (result ._data , name = result .name )
713
- if result .freq is None :
714
- result ._set_freq ("infer" )
787
+ result = result ._with_freq ("infer" )
715
788
return result
716
789
717
790
# to make our life easier, "sort" the two ranges
@@ -781,10 +854,9 @@ def _fast_union(self, other, sort=None):
781
854
left_start = left [0 ]
782
855
loc = right .searchsorted (left_start , side = "left" )
783
856
right_chunk = right ._values [:loc ]
784
- dates = concat_compat ([left ._values , right_chunk ])
785
- result = self ._shallow_copy (dates )
786
- result ._set_freq ("infer" )
857
+ dates = concat_compat ((left ._values , right_chunk ))
787
858
# TODO: can we infer that it has self.freq?
859
+ result = self ._shallow_copy (dates )._with_freq ("infer" )
788
860
return result
789
861
else :
790
862
left , right = other , self
@@ -797,9 +869,8 @@ def _fast_union(self, other, sort=None):
797
869
loc = right .searchsorted (left_end , side = "right" )
798
870
right_chunk = right ._values [loc :]
799
871
dates = concat_compat ([left ._values , right_chunk ])
800
- result = self ._shallow_copy (dates )
801
- result ._set_freq ("infer" )
802
872
# TODO: can we infer that it has self.freq?
873
+ result = self ._shallow_copy (dates )._with_freq ("infer" )
803
874
return result
804
875
else :
805
876
return left
@@ -816,7 +887,7 @@ def _union(self, other, sort):
816
887
if this ._can_fast_union (other ):
817
888
result = this ._fast_union (other , sort = sort )
818
889
if result .freq is None :
819
- result . _set_freq ("infer" )
890
+ result = result . _with_freq ("infer" )
820
891
return result
821
892
else :
822
893
i8self = Int64Index ._simple_new (self .asi8 , name = self .name )
0 commit comments