Skip to content

Commit 699b56f

Browse files
authored
BUG: TimedeltaArray+Period, PeriodArray-TimedeltaArray (#33883)
1 parent 4bd6905 commit 699b56f

File tree

6 files changed

+76
-16
lines changed

6 files changed

+76
-16
lines changed

doc/source/whatsnew/v1.1.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ Datetimelike
647647
- :meth:`DataFrame.min`/:meth:`DataFrame.max` not returning consistent result with :meth:`Series.min`/:meth:`Series.max` when called on objects initialized with empty :func:`pd.to_datetime`
648648
- Bug in :meth:`DatetimeIndex.intersection` and :meth:`TimedeltaIndex.intersection` with results not having the correct ``name`` attribute (:issue:`33904`)
649649
- Bug in :meth:`DatetimeArray.__setitem__`, :meth:`TimedeltaArray.__setitem__`, :meth:`PeriodArray.__setitem__` incorrectly allowing values with ``int64`` dtype to be silently cast (:issue:`33717`)
650+
- Bug in subtracting :class:`TimedeltaIndex` from :class:`Period` incorrectly raising ``TypeError`` in some cases where it should succeed and ``IncompatibleFrequency`` in some cases where it should raise ``TypeError`` (:issue:`33883`)
650651

651652
Timedelta
652653
^^^^^^^^^

pandas/core/arrays/datetimelike.py

+6
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,10 @@ def _sub_period(self, other):
11901190
# Overridden by PeriodArray
11911191
raise TypeError(f"cannot subtract Period from a {type(self).__name__}")
11921192

1193+
def _add_period(self, other: Period):
1194+
# Overriden by TimedeltaArray
1195+
raise TypeError(f"cannot add Period to a {type(self).__name__}")
1196+
11931197
def _add_offset(self, offset):
11941198
raise AbstractMethodError(self)
11951199

@@ -1364,6 +1368,8 @@ def __add__(self, other):
13641368
result = self._add_offset(other)
13651369
elif isinstance(other, (datetime, np.datetime64)):
13661370
result = self._add_datetimelike_scalar(other)
1371+
elif isinstance(other, Period) and is_timedelta64_dtype(self.dtype):
1372+
result = self._add_period(other)
13671373
elif lib.is_integer(other):
13681374
# This check must come after the check for np.timedelta64
13691375
# as is_integer returns True for these

pandas/core/arrays/period.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@
3232
pandas_dtype,
3333
)
3434
from pandas.core.dtypes.dtypes import PeriodDtype
35-
from pandas.core.dtypes.generic import ABCIndexClass, ABCPeriodIndex, ABCSeries
35+
from pandas.core.dtypes.generic import (
36+
ABCIndexClass,
37+
ABCPeriodIndex,
38+
ABCSeries,
39+
ABCTimedeltaArray,
40+
)
3641
from pandas.core.dtypes.missing import isna, notna
3742

3843
import pandas.core.algorithms as algos
@@ -707,7 +712,9 @@ def _add_timedelta_arraylike(self, other):
707712
"""
708713
if not isinstance(self.freq, Tick):
709714
# We cannot add timedelta-like to non-tick PeriodArray
710-
raise raise_on_incompatible(self, other)
715+
raise TypeError(
716+
f"Cannot add or subtract timedelta64[ns] dtype from {self.dtype}"
717+
)
711718

712719
if not np.all(isna(other)):
713720
delta = self._check_timedeltalike_freq_compat(other)
@@ -784,7 +791,7 @@ def raise_on_incompatible(left, right):
784791
Exception to be raised by the caller.
785792
"""
786793
# GH#24283 error message format depends on whether right is scalar
787-
if isinstance(right, np.ndarray) or right is None:
794+
if isinstance(right, (np.ndarray, ABCTimedeltaArray)) or right is None:
788795
other_freq = None
789796
elif isinstance(right, (ABCPeriodIndex, PeriodArray, Period, DateOffset)):
790797
other_freq = right.freqstr

pandas/core/arrays/timedeltas.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import numpy as np
55

66
from pandas._libs import lib, tslibs
7-
from pandas._libs.tslibs import NaT, Timedelta, Timestamp, iNaT
7+
from pandas._libs.tslibs import NaT, Period, Timedelta, Timestamp, iNaT
88
from pandas._libs.tslibs.fields import get_timedelta_field
99
from pandas._libs.tslibs.timedeltas import (
1010
array_to_timedelta64,
@@ -404,6 +404,17 @@ def _add_offset(self, other):
404404
f"cannot add the type {type(other).__name__} to a {type(self).__name__}"
405405
)
406406

407+
def _add_period(self, other: Period):
408+
"""
409+
Add a Period object.
410+
"""
411+
# We will wrap in a PeriodArray and defer to the reversed operation
412+
from .period import PeriodArray
413+
414+
i8vals = np.broadcast_to(other.ordinal, self.shape)
415+
oth = PeriodArray(i8vals, freq=other.freq)
416+
return oth + self
417+
407418
def _add_datetime_arraylike(self, other):
408419
"""
409420
Add DatetimeArray/Index or ndarray[datetime64] to TimedeltaArray.

pandas/tests/arithmetic/test_period.py

+47-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pandas.errors import PerformanceWarning
1111

1212
import pandas as pd
13-
from pandas import Period, PeriodIndex, Series, period_range
13+
from pandas import Period, PeriodIndex, Series, TimedeltaIndex, Timestamp, period_range
1414
import pandas._testing as tm
1515
from pandas.core import ops
1616
from pandas.core.arrays import TimedeltaArray
@@ -730,13 +730,13 @@ def test_pi_add_sub_td64_array_non_tick_raises(self):
730730
tdi = pd.TimedeltaIndex(["-1 Day", "-1 Day", "-1 Day"])
731731
tdarr = tdi.values
732732

733-
msg = r"Input has different freq=None from PeriodArray\(freq=Q-DEC\)"
734-
with pytest.raises(IncompatibleFrequency, match=msg):
733+
msg = r"Cannot add or subtract timedelta64\[ns\] dtype from period\[Q-DEC\]"
734+
with pytest.raises(TypeError, match=msg):
735735
rng + tdarr
736-
with pytest.raises(IncompatibleFrequency, match=msg):
736+
with pytest.raises(TypeError, match=msg):
737737
tdarr + rng
738738

739-
with pytest.raises(IncompatibleFrequency, match=msg):
739+
with pytest.raises(TypeError, match=msg):
740740
rng - tdarr
741741
msg = r"cannot subtract PeriodArray from timedelta64\[ns\]"
742742
with pytest.raises(TypeError, match=msg):
@@ -773,6 +773,48 @@ def test_pi_add_sub_td64_array_tick(self):
773773
with pytest.raises(TypeError, match=msg):
774774
tdi - rng
775775

776+
@pytest.mark.parametrize("pi_freq", ["D", "W", "Q", "H"])
777+
@pytest.mark.parametrize("tdi_freq", [None, "H"])
778+
def test_parr_sub_td64array(self, box_with_array, tdi_freq, pi_freq):
779+
box = box_with_array
780+
xbox = box if box is not tm.to_array else pd.Index
781+
782+
tdi = TimedeltaIndex(["1 hours", "2 hours"], freq=tdi_freq)
783+
dti = Timestamp("2018-03-07 17:16:40") + tdi
784+
pi = dti.to_period(pi_freq)
785+
786+
# TODO: parametrize over box for pi?
787+
td64obj = tm.box_expected(tdi, box)
788+
789+
if pi_freq == "H":
790+
result = pi - td64obj
791+
expected = (pi.to_timestamp("S") - tdi).to_period(pi_freq)
792+
expected = tm.box_expected(expected, xbox)
793+
tm.assert_equal(result, expected)
794+
795+
# Subtract from scalar
796+
result = pi[0] - td64obj
797+
expected = (pi[0].to_timestamp("S") - tdi).to_period(pi_freq)
798+
expected = tm.box_expected(expected, box)
799+
tm.assert_equal(result, expected)
800+
801+
elif pi_freq == "D":
802+
# Tick, but non-compatible
803+
msg = "Input has different freq=None from PeriodArray"
804+
with pytest.raises(IncompatibleFrequency, match=msg):
805+
pi - td64obj
806+
with pytest.raises(IncompatibleFrequency, match=msg):
807+
pi[0] - td64obj
808+
809+
else:
810+
# With non-Tick freq, we could not add timedelta64 array regardless
811+
# of what its resolution is
812+
msg = "Cannot add or subtract timedelta64"
813+
with pytest.raises(TypeError, match=msg):
814+
pi - td64obj
815+
with pytest.raises(TypeError, match=msg):
816+
pi[0] - td64obj
817+
776818
# -----------------------------------------------------------------
777819
# operations with array/Index of DateOffset objects
778820

pandas/tests/arithmetic/test_timedelta64.py

-7
Original file line numberDiff line numberDiff line change
@@ -1081,16 +1081,9 @@ def test_td64arr_sub_periodlike(self, box_with_array, tdi_freq, pi_freq):
10811081
with pytest.raises(TypeError, match=msg):
10821082
tdi - pi
10831083

1084-
# FIXME: don't leave commented-out
1085-
# FIXME: this raises with period scalar but not with PeriodIndex?
1086-
# with pytest.raises(TypeError):
1087-
# pi - tdi
1088-
10891084
# GH#13078 subtraction of Period scalar not supported
10901085
with pytest.raises(TypeError, match=msg):
10911086
tdi - pi[0]
1092-
with pytest.raises(TypeError, match=msg):
1093-
pi[0] - tdi
10941087

10951088
@pytest.mark.parametrize(
10961089
"other",

0 commit comments

Comments
 (0)