Skip to content

Commit 4f4282f

Browse files
authored
BUG: NaT+Period doesnt match Period+NaT (#34242)
1 parent 8cd8ed3 commit 4f4282f

File tree

3 files changed

+64
-55
lines changed

3 files changed

+64
-55
lines changed

pandas/_libs/tslibs/period.pyx

+46-50
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ from pandas._libs.tslibs.ccalendar cimport (
5252
from pandas._libs.tslibs.ccalendar cimport c_MONTH_NUMBERS
5353
from pandas._libs.tslibs.frequencies cimport (
5454
attrname_to_abbrevs,
55-
get_base_alias,
5655
get_freq_code,
5756
get_freq_str,
5857
get_rule_month,
@@ -1600,9 +1599,7 @@ cdef class _Period:
16001599
raise IncompatibleFrequency("Input cannot be converted to "
16011600
f"Period(freq={self.freqstr})")
16021601
elif util.is_offset_object(other):
1603-
freqstr = other.rule_code
1604-
base = get_base_alias(freqstr)
1605-
if base == self.freq.rule_code:
1602+
if other.base == self.freq.base:
16061603
ordinal = self.ordinal + other.n
16071604
return Period(ordinal=ordinal, freq=self.freq)
16081605
msg = DIFFERENT_FREQ.format(cls=type(self).__name__,
@@ -1613,58 +1610,57 @@ cdef class _Period:
16131610
return NotImplemented
16141611

16151612
def __add__(self, other):
1616-
if is_period_object(self):
1617-
if (PyDelta_Check(other) or util.is_timedelta64_object(other) or
1618-
util.is_offset_object(other)):
1619-
return self._add_delta(other)
1620-
elif other is NaT:
1613+
if not is_period_object(self):
1614+
# cython semantics; this is analogous to a call to __radd__
1615+
if self is NaT:
16211616
return NaT
1622-
elif util.is_integer_object(other):
1623-
ordinal = self.ordinal + other * self.freq.n
1624-
return Period(ordinal=ordinal, freq=self.freq)
1625-
elif (PyDateTime_Check(other) or
1626-
is_period_object(other) or util.is_datetime64_object(other)):
1627-
# can't add datetime-like
1628-
# GH#17983
1629-
sname = type(self).__name__
1630-
oname = type(other).__name__
1631-
raise TypeError(f"unsupported operand type(s) for +: '{sname}' "
1632-
f"and '{oname}'")
1633-
else: # pragma: no cover
1634-
return NotImplemented
1635-
elif is_period_object(other):
1636-
# this can be reached via __radd__ because of cython rules
1637-
return other + self
1638-
else:
1639-
return NotImplemented
1617+
return other.__add__(self)
1618+
1619+
if (PyDelta_Check(other) or util.is_timedelta64_object(other) or
1620+
util.is_offset_object(other)):
1621+
return self._add_delta(other)
1622+
elif other is NaT:
1623+
return NaT
1624+
elif util.is_integer_object(other):
1625+
ordinal = self.ordinal + other * self.freq.n
1626+
return Period(ordinal=ordinal, freq=self.freq)
1627+
elif (PyDateTime_Check(other) or
1628+
is_period_object(other) or util.is_datetime64_object(other)):
1629+
# can't add datetime-like
1630+
# GH#17983
1631+
sname = type(self).__name__
1632+
oname = type(other).__name__
1633+
raise TypeError(f"unsupported operand type(s) for +: '{sname}' "
1634+
f"and '{oname}'")
1635+
1636+
return NotImplemented
16401637

16411638
def __sub__(self, other):
1642-
if is_period_object(self):
1643-
if (PyDelta_Check(other) or util.is_timedelta64_object(other) or
1644-
util.is_offset_object(other)):
1645-
neg_other = -other
1646-
return self + neg_other
1647-
elif util.is_integer_object(other):
1648-
ordinal = self.ordinal - other * self.freq.n
1649-
return Period(ordinal=ordinal, freq=self.freq)
1650-
elif is_period_object(other):
1651-
if other.freq != self.freq:
1652-
msg = DIFFERENT_FREQ.format(cls=type(self).__name__,
1653-
own_freq=self.freqstr,
1654-
other_freq=other.freqstr)
1655-
raise IncompatibleFrequency(msg)
1656-
# GH 23915 - mul by base freq since __add__ is agnostic of n
1657-
return (self.ordinal - other.ordinal) * self.freq.base
1658-
elif other is NaT:
1659-
return NaT
1660-
return NotImplemented
1661-
elif is_period_object(other):
1662-
# this can be reached via __rsub__ because of cython rules
1639+
if not is_period_object(self):
1640+
# cython semantics; this is like a call to __rsub__
16631641
if self is NaT:
16641642
return NaT
16651643
return NotImplemented
1666-
else:
1667-
return NotImplemented
1644+
1645+
elif (PyDelta_Check(other) or util.is_timedelta64_object(other) or
1646+
util.is_offset_object(other)):
1647+
neg_other = -other
1648+
return self + neg_other
1649+
elif util.is_integer_object(other):
1650+
ordinal = self.ordinal - other * self.freq.n
1651+
return Period(ordinal=ordinal, freq=self.freq)
1652+
elif is_period_object(other):
1653+
if other.freq != self.freq:
1654+
msg = DIFFERENT_FREQ.format(cls=type(self).__name__,
1655+
own_freq=self.freqstr,
1656+
other_freq=other.freqstr)
1657+
raise IncompatibleFrequency(msg)
1658+
# GH 23915 - mul by base freq since __add__ is agnostic of n
1659+
return (self.ordinal - other.ordinal) * self.freq.base
1660+
elif other is NaT:
1661+
return NaT
1662+
1663+
return NotImplemented
16681664

16691665
def asfreq(self, freq, how='E') -> "Period":
16701666
"""

pandas/core/arrays/period.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,8 @@ def _addsub_int_array(
667667

668668
def _add_offset(self, other):
669669
assert not isinstance(other, Tick)
670-
base = libfrequencies.get_base_alias(other.rule_code)
671-
if base != self.freq.rule_code:
670+
671+
if other.base != self.freq.base:
672672
raise raise_on_incompatible(self, other)
673673

674674
# Note: when calling parent class's _add_timedeltalike_scalar,

pandas/tests/scalar/period/test_period.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,13 @@ def test_add_invalid(self):
10611061
per1 = Period(freq="D", year=2008, month=1, day=1)
10621062
per2 = Period(freq="D", year=2008, month=1, day=2)
10631063

1064-
msg = r"unsupported operand type\(s\)"
1064+
msg = "|".join(
1065+
[
1066+
r"unsupported operand type\(s\)",
1067+
"can only concatenate str",
1068+
"must be str, not Period",
1069+
]
1070+
)
10651071
with pytest.raises(TypeError, match=msg):
10661072
per1 + "str"
10671073
with pytest.raises(TypeError, match=msg):
@@ -1402,8 +1408,15 @@ def test_sub_offset(self):
14021408

14031409
@pytest.mark.parametrize("freq", ["M", "2M", "3M"])
14041410
def test_period_addsub_nat(self, freq):
1405-
assert NaT - Period("2011-01", freq=freq) is NaT
1406-
assert Period("2011-01", freq=freq) - NaT is NaT
1411+
per = Period("2011-01", freq=freq)
1412+
1413+
# For subtraction, NaT is treated as another Period object
1414+
assert NaT - per is NaT
1415+
assert per - NaT is NaT
1416+
1417+
# For addition, NaT is treated as offset-like
1418+
assert NaT + per is NaT
1419+
assert per + NaT is NaT
14071420

14081421
def test_period_ops_offset(self):
14091422
p = Period("2011-04-01", freq="D")

0 commit comments

Comments
 (0)