Skip to content

Commit e6f1537

Browse files
authored
REF: de-duplicate PeriodArray arithmetic code (pandas-dev#47826)
1 parent 4d7cfc4 commit e6f1537

File tree

2 files changed

+57
-48
lines changed

2 files changed

+57
-48
lines changed

pandas/core/arrays/period.py

+30-41
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,17 @@
1414

1515
import numpy as np
1616

17-
from pandas._libs import algos as libalgos
17+
from pandas._libs import (
18+
algos as libalgos,
19+
lib,
20+
)
1821
from pandas._libs.arrays import NDArrayBacked
1922
from pandas._libs.tslibs import (
2023
BaseOffset,
2124
NaT,
2225
NaTType,
2326
Timedelta,
2427
astype_overflowsafe,
25-
delta_to_nanoseconds,
2628
dt64arr_to_periodarr as c_dt64arr_to_periodarr,
2729
get_unit_from_dtype,
2830
iNaT,
@@ -55,7 +57,6 @@
5557
)
5658

5759
from pandas.core.dtypes.common import (
58-
TD64NS_DTYPE,
5960
ensure_object,
6061
is_datetime64_any_dtype,
6162
is_datetime64_dtype,
@@ -72,7 +73,7 @@
7273
ABCSeries,
7374
ABCTimedeltaArray,
7475
)
75-
from pandas.core.dtypes.missing import notna
76+
from pandas.core.dtypes.missing import isna
7677

7778
import pandas.core.algorithms as algos
7879
from pandas.core.arrays import datetimelike as dtl
@@ -764,30 +765,24 @@ def _add_timedeltalike_scalar(self, other):
764765
# We cannot add timedelta-like to non-tick PeriodArray
765766
raise raise_on_incompatible(self, other)
766767

767-
if notna(other):
768-
# Convert to an integer increment of our own freq, disallowing
769-
# e.g. 30seconds if our freq is minutes.
770-
try:
771-
inc = delta_to_nanoseconds(other, reso=self.freq._reso, round_ok=False)
772-
except ValueError as err:
773-
# "Cannot losslessly convert units"
774-
raise raise_on_incompatible(self, other) from err
775-
776-
return self._addsub_int_array_or_scalar(inc, operator.add)
768+
if isna(other):
769+
# i.e. np.timedelta64("NaT")
770+
return super()._add_timedeltalike_scalar(other)
777771

778-
return super()._add_timedeltalike_scalar(other)
772+
td = np.asarray(Timedelta(other).asm8)
773+
return self._add_timedelta_arraylike(td)
779774

780775
def _add_timedelta_arraylike(
781776
self, other: TimedeltaArray | npt.NDArray[np.timedelta64]
782-
):
777+
) -> PeriodArray:
783778
"""
784779
Parameters
785780
----------
786781
other : TimedeltaArray or ndarray[timedelta64]
787782
788783
Returns
789784
-------
790-
result : ndarray[int64]
785+
PeriodArray
791786
"""
792787
freq = self.freq
793788
if not isinstance(freq, Tick):
@@ -803,8 +798,12 @@ def _add_timedelta_arraylike(
803798
np.asarray(other), dtype=dtype, copy=False, round_ok=False
804799
)
805800
except ValueError as err:
806-
# TODO: not actually a great exception message in this case
807-
raise raise_on_incompatible(self, other) from err
801+
# e.g. if we have minutes freq and try to add 30s
802+
# "Cannot losslessly convert units"
803+
raise IncompatibleFrequency(
804+
"Cannot add/subtract timedelta-like from PeriodArray that is "
805+
"not an integer multiple of the PeriodArray's freq."
806+
) from err
808807

809808
b_mask = np.isnat(delta)
810809

@@ -835,31 +834,21 @@ def _check_timedeltalike_freq_compat(self, other):
835834
IncompatibleFrequency
836835
"""
837836
assert isinstance(self.freq, Tick) # checked by calling function
838-
base_nanos = self.freq.base.nanos
837+
838+
dtype = np.dtype(f"m8[{self.freq._td64_unit}]")
839839

840840
if isinstance(other, (timedelta, np.timedelta64, Tick)):
841-
nanos = delta_to_nanoseconds(other)
842-
843-
elif isinstance(other, np.ndarray):
844-
# numpy timedelta64 array; all entries must be compatible
845-
assert other.dtype.kind == "m"
846-
other = astype_overflowsafe(other, TD64NS_DTYPE, copy=False)
847-
# error: Incompatible types in assignment (expression has type
848-
# "ndarray[Any, dtype[Any]]", variable has type "int")
849-
nanos = other.view("i8") # type: ignore[assignment]
841+
td = np.asarray(Timedelta(other).asm8)
850842
else:
851-
# TimedeltaArray/Index
852-
nanos = other.asi8
853-
854-
if np.all(nanos % base_nanos == 0):
855-
# nanos being added is an integer multiple of the
856-
# base-frequency to self.freq
857-
delta = nanos // base_nanos
858-
# delta is the integer (or integer-array) number of periods
859-
# by which will be added to self.
860-
return delta
861-
862-
raise raise_on_incompatible(self, other)
843+
td = np.asarray(other)
844+
845+
try:
846+
delta = astype_overflowsafe(td, dtype=dtype, copy=False, round_ok=False)
847+
except ValueError as err:
848+
raise raise_on_incompatible(self, other) from err
849+
850+
delta = delta.view("i8")
851+
return lib.item_from_zerodim(delta)
863852

864853

865854
def raise_on_incompatible(left, right):

pandas/tests/arithmetic/test_period.py

+27-7
Original file line numberDiff line numberDiff line change
@@ -807,9 +807,13 @@ def test_parr_sub_td64array(self, box_with_array, tdi_freq, pi_freq):
807807

808808
elif pi_freq == "D":
809809
# Tick, but non-compatible
810-
msg = "Input has different freq=None from PeriodArray"
810+
msg = (
811+
"Cannot add/subtract timedelta-like from PeriodArray that is "
812+
"not an integer multiple of the PeriodArray's freq."
813+
)
811814
with pytest.raises(IncompatibleFrequency, match=msg):
812815
pi - td64obj
816+
813817
with pytest.raises(IncompatibleFrequency, match=msg):
814818
pi[0] - td64obj
815819

@@ -1107,7 +1111,15 @@ def test_parr_add_sub_timedeltalike_freq_mismatch_daily(
11071111
rng = period_range("2014-05-01", "2014-05-15", freq="D")
11081112
rng = tm.box_expected(rng, box_with_array)
11091113

1110-
msg = "Input has different freq(=.+)? from Period.*?\\(freq=D\\)"
1114+
msg = "|".join(
1115+
[
1116+
# non-timedelta-like DateOffset
1117+
"Input has different freq(=.+)? from Period.*?\\(freq=D\\)",
1118+
# timedelta/td64/Timedelta but not a multiple of 24H
1119+
"Cannot add/subtract timedelta-like from PeriodArray that is "
1120+
"not an integer multiple of the PeriodArray's freq.",
1121+
]
1122+
)
11111123
with pytest.raises(IncompatibleFrequency, match=msg):
11121124
rng + other
11131125
with pytest.raises(IncompatibleFrequency, match=msg):
@@ -1134,7 +1146,15 @@ def test_parr_add_timedeltalike_mismatched_freq_hourly(
11341146
other = not_hourly
11351147
rng = period_range("2014-01-01 10:00", "2014-01-05 10:00", freq="H")
11361148
rng = tm.box_expected(rng, box_with_array)
1137-
msg = "Input has different freq(=.+)? from Period.*?\\(freq=H\\)"
1149+
msg = "|".join(
1150+
[
1151+
# non-timedelta-like DateOffset
1152+
"Input has different freq(=.+)? from Period.*?\\(freq=H\\)",
1153+
# timedelta/td64/Timedelta but not a multiple of 24H
1154+
"Cannot add/subtract timedelta-like from PeriodArray that is "
1155+
"not an integer multiple of the PeriodArray's freq.",
1156+
]
1157+
)
11381158

11391159
with pytest.raises(IncompatibleFrequency, match=msg):
11401160
rng + other
@@ -1508,17 +1528,17 @@ def test_pi_offset_errors(self):
15081528
)
15091529
ser = Series(idx)
15101530

1511-
# Series op is applied per Period instance, thus error is raised
1512-
# from Period
1531+
msg = (
1532+
"Cannot add/subtract timedelta-like from PeriodArray that is not "
1533+
"an integer multiple of the PeriodArray's freq"
1534+
)
15131535
for obj in [idx, ser]:
1514-
msg = r"Input has different freq=2H from Period.*?\(freq=D\)"
15151536
with pytest.raises(IncompatibleFrequency, match=msg):
15161537
obj + pd.offsets.Hour(2)
15171538

15181539
with pytest.raises(IncompatibleFrequency, match=msg):
15191540
pd.offsets.Hour(2) + obj
15201541

1521-
msg = r"Input has different freq=-2H from Period.*?\(freq=D\)"
15221542
with pytest.raises(IncompatibleFrequency, match=msg):
15231543
obj - pd.offsets.Hour(2)
15241544

0 commit comments

Comments
 (0)