4
4
datetime ,
5
5
timedelta ,
6
6
)
7
+ from functools import wraps
7
8
import operator
8
9
from typing import (
9
10
TYPE_CHECKING ,
57
58
DatetimeLikeScalar ,
58
59
Dtype ,
59
60
DtypeObj ,
61
+ F ,
60
62
NpDtype ,
61
63
PositionalIndexer2D ,
62
64
PositionalIndexerTuple ,
157
159
DatetimeLikeArrayT = TypeVar ("DatetimeLikeArrayT" , bound = "DatetimeLikeArrayMixin" )
158
160
159
161
162
+ def _period_dispatch (meth : F ) -> F :
163
+ """
164
+ For PeriodArray methods, dispatch to DatetimeArray and re-wrap the results
165
+ in PeriodArray. We cannot use ._ndarray directly for the affected
166
+ methods because the i8 data has different semantics on NaT values.
167
+ """
168
+
169
+ @wraps (meth )
170
+ def new_meth (self , * args , ** kwargs ):
171
+ if not is_period_dtype (self .dtype ):
172
+ return meth (self , * args , ** kwargs )
173
+
174
+ arr = self .view ("M8[ns]" )
175
+ result = meth (arr , * args , ** kwargs )
176
+ if result is NaT :
177
+ return NaT
178
+ elif isinstance (result , Timestamp ):
179
+ return self ._box_func (result .value )
180
+
181
+ res_i8 = result .view ("i8" )
182
+ return self ._from_backing_data (res_i8 )
183
+
184
+ return cast (F , new_meth )
185
+
186
+
160
187
class DatetimeLikeArrayMixin (OpsMixin , NDArrayBackedExtensionArray ):
161
188
"""
162
189
Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray
@@ -1546,6 +1573,15 @@ def __isub__(self: DatetimeLikeArrayT, other) -> DatetimeLikeArrayT:
1546
1573
# --------------------------------------------------------------
1547
1574
# Reductions
1548
1575
1576
+ @_period_dispatch
1577
+ def _quantile (
1578
+ self : DatetimeLikeArrayT ,
1579
+ qs : npt .NDArray [np .float64 ],
1580
+ interpolation : str ,
1581
+ ) -> DatetimeLikeArrayT :
1582
+ return super ()._quantile (qs = qs , interpolation = interpolation )
1583
+
1584
+ @_period_dispatch
1549
1585
def min (self , * , axis : AxisInt | None = None , skipna : bool = True , ** kwargs ):
1550
1586
"""
1551
1587
Return the minimum value of the Array or minimum along
@@ -1560,21 +1596,10 @@ def min(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs):
1560
1596
nv .validate_min ((), kwargs )
1561
1597
nv .validate_minmax_axis (axis , self .ndim )
1562
1598
1563
- if is_period_dtype (self .dtype ):
1564
- # pass datetime64 values to nanops to get correct NaT semantics
1565
- result = nanops .nanmin (
1566
- self ._ndarray .view ("M8[ns]" ), axis = axis , skipna = skipna
1567
- )
1568
- if result is NaT :
1569
- return NaT
1570
- result = result .view ("i8" )
1571
- if axis is None or self .ndim == 1 :
1572
- return self ._box_func (result )
1573
- return self ._from_backing_data (result )
1574
-
1575
1599
result = nanops .nanmin (self ._ndarray , axis = axis , skipna = skipna )
1576
1600
return self ._wrap_reduction_result (axis , result )
1577
1601
1602
+ @_period_dispatch
1578
1603
def max (self , * , axis : AxisInt | None = None , skipna : bool = True , ** kwargs ):
1579
1604
"""
1580
1605
Return the maximum value of the Array or maximum along
@@ -1589,18 +1614,6 @@ def max(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs):
1589
1614
nv .validate_max ((), kwargs )
1590
1615
nv .validate_minmax_axis (axis , self .ndim )
1591
1616
1592
- if is_period_dtype (self .dtype ):
1593
- # pass datetime64 values to nanops to get correct NaT semantics
1594
- result = nanops .nanmax (
1595
- self ._ndarray .view ("M8[ns]" ), axis = axis , skipna = skipna
1596
- )
1597
- if result is NaT :
1598
- return result
1599
- result = result .view ("i8" )
1600
- if axis is None or self .ndim == 1 :
1601
- return self ._box_func (result )
1602
- return self ._from_backing_data (result )
1603
-
1604
1617
result = nanops .nanmax (self ._ndarray , axis = axis , skipna = skipna )
1605
1618
return self ._wrap_reduction_result (axis , result )
1606
1619
@@ -1641,22 +1654,13 @@ def mean(self, *, skipna: bool = True, axis: AxisInt | None = 0):
1641
1654
)
1642
1655
return self ._wrap_reduction_result (axis , result )
1643
1656
1657
+ @_period_dispatch
1644
1658
def median (self , * , axis : AxisInt | None = None , skipna : bool = True , ** kwargs ):
1645
1659
nv .validate_median ((), kwargs )
1646
1660
1647
1661
if axis is not None and abs (axis ) >= self .ndim :
1648
1662
raise ValueError ("abs(axis) must be less than ndim" )
1649
1663
1650
- if is_period_dtype (self .dtype ):
1651
- # pass datetime64 values to nanops to get correct NaT semantics
1652
- result = nanops .nanmedian (
1653
- self ._ndarray .view ("M8[ns]" ), axis = axis , skipna = skipna
1654
- )
1655
- result = result .view ("i8" )
1656
- if axis is None or self .ndim == 1 :
1657
- return self ._box_func (result )
1658
- return self ._from_backing_data (result )
1659
-
1660
1664
result = nanops .nanmedian (self ._ndarray , axis = axis , skipna = skipna )
1661
1665
return self ._wrap_reduction_result (axis , result )
1662
1666
0 commit comments