Skip to content

Commit 63f99c2

Browse files
authored
REF: share start_time/end_time between Period/PeriodArray (#39279)
1 parent 30276bf commit 63f99c2

File tree

3 files changed

+70
-74
lines changed

3 files changed

+70
-74
lines changed

pandas/_libs/tslibs/period.pyx

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,60 @@ cdef class PeriodMixin:
14891489
return FR_SEC
14901490
return base
14911491

1492+
@property
1493+
def start_time(self) -> Timestamp:
1494+
"""
1495+
Get the Timestamp for the start of the period.
1496+
1497+
Returns
1498+
-------
1499+
Timestamp
1500+
1501+
See Also
1502+
--------
1503+
Period.end_time : Return the end Timestamp.
1504+
Period.dayofyear : Return the day of year.
1505+
Period.daysinmonth : Return the days in that month.
1506+
Period.dayofweek : Return the day of the week.
1507+
1508+
Examples
1509+
--------
1510+
>>> period = pd.Period('2012-1-1', freq='D')
1511+
>>> period
1512+
Period('2012-01-01', 'D')
1513+
1514+
>>> period.start_time
1515+
Timestamp('2012-01-01 00:00:00')
1516+
1517+
>>> period.end_time
1518+
Timestamp('2012-01-01 23:59:59.999999999')
1519+
"""
1520+
return self.to_timestamp(how="start")
1521+
1522+
@property
1523+
def end_time(self) -> Timestamp:
1524+
return self.to_timestamp(how="end")
1525+
1526+
def _require_matching_freq(self, other, base=False):
1527+
# See also arrays.period.raise_on_incompatible
1528+
if is_offset_object(other):
1529+
other_freq = other
1530+
else:
1531+
other_freq = other.freq
1532+
1533+
if base:
1534+
condition = self.freq.base != other_freq.base
1535+
else:
1536+
condition = self.freq != other_freq
1537+
1538+
if condition:
1539+
msg = DIFFERENT_FREQ.format(
1540+
cls=type(self).__name__,
1541+
own_freq=self.freqstr,
1542+
other_freq=other_freq.freqstr,
1543+
)
1544+
raise IncompatibleFrequency(msg)
1545+
14921546

14931547
cdef class _Period(PeriodMixin):
14941548

@@ -1551,10 +1605,7 @@ cdef class _Period(PeriodMixin):
15511605
return False
15521606
elif op == Py_NE:
15531607
return True
1554-
msg = DIFFERENT_FREQ.format(cls=type(self).__name__,
1555-
own_freq=self.freqstr,
1556-
other_freq=other.freqstr)
1557-
raise IncompatibleFrequency(msg)
1608+
self._require_matching_freq(other)
15581609
return PyObject_RichCompareBool(self.ordinal, other.ordinal, op)
15591610
elif other is NaT:
15601611
return _nat_scalar_rules[op]
@@ -1563,15 +1614,15 @@ cdef class _Period(PeriodMixin):
15631614
def __hash__(self):
15641615
return hash((self.ordinal, self.freqstr))
15651616

1566-
def _add_delta(self, other) -> "Period":
1617+
def _add_timedeltalike_scalar(self, other) -> "Period":
15671618
cdef:
1568-
int64_t nanos, offset_nanos
1619+
int64_t nanos, base_nanos
15691620

15701621
if is_tick_object(self.freq):
15711622
nanos = delta_to_nanoseconds(other)
1572-
offset_nanos = self.freq.base.nanos
1573-
if nanos % offset_nanos == 0:
1574-
ordinal = self.ordinal + (nanos // offset_nanos)
1623+
base_nanos = self.freq.base.nanos
1624+
if nanos % base_nanos == 0:
1625+
ordinal = self.ordinal + (nanos // base_nanos)
15751626
return Period(ordinal=ordinal, freq=self.freq)
15761627
raise IncompatibleFrequency("Input cannot be converted to "
15771628
f"Period(freq={self.freqstr})")
@@ -1581,14 +1632,10 @@ cdef class _Period(PeriodMixin):
15811632
cdef:
15821633
int64_t ordinal
15831634

1584-
if other.base == self.freq.base:
1585-
ordinal = self.ordinal + other.n
1586-
return Period(ordinal=ordinal, freq=self.freq)
1635+
self._require_matching_freq(other, base=True)
15871636

1588-
msg = DIFFERENT_FREQ.format(cls=type(self).__name__,
1589-
own_freq=self.freqstr,
1590-
other_freq=other.freqstr)
1591-
raise IncompatibleFrequency(msg)
1637+
ordinal = self.ordinal + other.n
1638+
return Period(ordinal=ordinal, freq=self.freq)
15921639

15931640
def __add__(self, other):
15941641
if not is_period_object(self):
@@ -1598,7 +1645,7 @@ cdef class _Period(PeriodMixin):
15981645
return other.__add__(self)
15991646

16001647
if is_any_td_scalar(other):
1601-
return self._add_delta(other)
1648+
return self._add_timedeltalike_scalar(other)
16021649
elif is_offset_object(other):
16031650
return self._add_offset(other)
16041651
elif other is NaT:
@@ -1635,11 +1682,7 @@ cdef class _Period(PeriodMixin):
16351682
ordinal = self.ordinal - other * self.freq.n
16361683
return Period(ordinal=ordinal, freq=self.freq)
16371684
elif is_period_object(other):
1638-
if other.freq != self.freq:
1639-
msg = DIFFERENT_FREQ.format(cls=type(self).__name__,
1640-
own_freq=self.freqstr,
1641-
other_freq=other.freqstr)
1642-
raise IncompatibleFrequency(msg)
1685+
self._require_matching_freq(other)
16431686
# GH 23915 - mul by base freq since __add__ is agnostic of n
16441687
return (self.ordinal - other.ordinal) * self.freq.base
16451688
elif other is NaT:
@@ -1677,40 +1720,6 @@ cdef class _Period(PeriodMixin):
16771720

16781721
return Period(ordinal=ordinal, freq=freq)
16791722

1680-
@property
1681-
def start_time(self) -> Timestamp:
1682-
"""
1683-
Get the Timestamp for the start of the period.
1684-
1685-
Returns
1686-
-------
1687-
Timestamp
1688-
1689-
See Also
1690-
--------
1691-
Period.end_time : Return the end Timestamp.
1692-
Period.dayofyear : Return the day of year.
1693-
Period.daysinmonth : Return the days in that month.
1694-
Period.dayofweek : Return the day of the week.
1695-
1696-
Examples
1697-
--------
1698-
>>> period = pd.Period('2012-1-1', freq='D')
1699-
>>> period
1700-
Period('2012-01-01', 'D')
1701-
1702-
>>> period.start_time
1703-
Timestamp('2012-01-01 00:00:00')
1704-
1705-
>>> period.end_time
1706-
Timestamp('2012-01-01 23:59:59.999999999')
1707-
"""
1708-
return self.to_timestamp(how='S')
1709-
1710-
@property
1711-
def end_time(self) -> Timestamp:
1712-
return self.to_timestamp(how="end")
1713-
17141723
def to_timestamp(self, freq=None, how='start', tz=None) -> Timestamp:
17151724
"""
17161725
Return the Timestamp representation of the Period.

pandas/core/arrays/period.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,7 @@ def _scalar_from_string(self, value: str) -> Period:
289289
def _check_compatible_with(self, other, setitem: bool = False):
290290
if other is NaT:
291291
return
292-
if self.freqstr != other.freqstr:
293-
raise raise_on_incompatible(self, other)
292+
self._require_matching_freq(other)
294293

295294
# --------------------------------------------------------------------
296295
# Data / Attributes
@@ -425,14 +424,6 @@ def is_leap_year(self) -> np.ndarray:
425424
"""
426425
return isleapyear_arr(np.asarray(self.year))
427426

428-
@property
429-
def start_time(self):
430-
return self.to_timestamp(how="start")
431-
432-
@property
433-
def end_time(self):
434-
return self.to_timestamp(how="end")
435-
436427
def to_timestamp(self, freq=None, how="start"):
437428
"""
438429
Cast to DatetimeArray/Index.
@@ -659,11 +650,7 @@ def _sub_period_array(self, other):
659650
result : np.ndarray[object]
660651
Array of DateOffset objects; nulls represented by NaT.
661652
"""
662-
if self.freq != other.freq:
663-
msg = DIFFERENT_FREQ.format(
664-
cls=type(self).__name__, own_freq=self.freqstr, other_freq=other.freqstr
665-
)
666-
raise IncompatibleFrequency(msg)
653+
self._require_matching_freq(other)
667654

668655
new_values = algos.checked_add_with_arr(
669656
self.asi8, -other.asi8, arr_mask=self._isnan, b_mask=other._isnan
@@ -702,8 +689,7 @@ def _addsub_int_array(
702689
def _add_offset(self, other: BaseOffset):
703690
assert not isinstance(other, Tick)
704691

705-
if other.base != self.freq.base:
706-
raise raise_on_incompatible(self, other)
692+
self._require_matching_freq(other, base=True)
707693

708694
# Note: when calling parent class's _add_timedeltalike_scalar,
709695
# it will call delta_to_nanoseconds(delta). Because delta here

pandas/core/indexes/extension.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ def inherit_from_data(name: str, delegate, cache: bool = False, wrap: bool = Fal
4242
"""
4343
attr = getattr(delegate, name)
4444

45-
if isinstance(attr, property):
45+
if isinstance(attr, property) or type(attr).__name__ == "getset_descriptor":
46+
# getset_descriptor i.e. property defined in cython class
4647
if cache:
4748

4849
def cached(self):

0 commit comments

Comments
 (0)