diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 8a62ad4e3d033..e276515ca1ae9 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1489,6 +1489,60 @@ cdef class PeriodMixin: return FR_SEC return base + @property + def start_time(self) -> Timestamp: + """ + Get the Timestamp for the start of the period. + + Returns + ------- + Timestamp + + See Also + -------- + Period.end_time : Return the end Timestamp. + Period.dayofyear : Return the day of year. + Period.daysinmonth : Return the days in that month. + Period.dayofweek : Return the day of the week. + + Examples + -------- + >>> period = pd.Period('2012-1-1', freq='D') + >>> period + Period('2012-01-01', 'D') + + >>> period.start_time + Timestamp('2012-01-01 00:00:00') + + >>> period.end_time + Timestamp('2012-01-01 23:59:59.999999999') + """ + return self.to_timestamp(how="start") + + @property + def end_time(self) -> Timestamp: + return self.to_timestamp(how="end") + + def _require_matching_freq(self, other, base=False): + # See also arrays.period.raise_on_incompatible + if is_offset_object(other): + other_freq = other + else: + other_freq = other.freq + + if base: + condition = self.freq.base != other_freq.base + else: + condition = self.freq != other_freq + + if condition: + msg = DIFFERENT_FREQ.format( + cls=type(self).__name__, + own_freq=self.freqstr, + other_freq=other_freq.freqstr, + ) + raise IncompatibleFrequency(msg) + cdef class _Period(PeriodMixin): @@ -1551,10 +1605,7 @@ cdef class _Period(PeriodMixin): return False elif op == Py_NE: return True - msg = DIFFERENT_FREQ.format(cls=type(self).__name__, - own_freq=self.freqstr, - other_freq=other.freqstr) - raise IncompatibleFrequency(msg) + self._require_matching_freq(other) return PyObject_RichCompareBool(self.ordinal, other.ordinal, op) elif other is NaT: return _nat_scalar_rules[op] @@ -1563,15 +1614,15 @@ cdef class _Period(PeriodMixin): def __hash__(self): return hash((self.ordinal, self.freqstr)) - def _add_delta(self, other) -> "Period": + def _add_timedeltalike_scalar(self, other) -> "Period": cdef: - int64_t nanos, offset_nanos + int64_t nanos, base_nanos if is_tick_object(self.freq): nanos = delta_to_nanoseconds(other) - offset_nanos = self.freq.base.nanos - if nanos % offset_nanos == 0: - ordinal = self.ordinal + (nanos // offset_nanos) + base_nanos = self.freq.base.nanos + if nanos % base_nanos == 0: + ordinal = self.ordinal + (nanos // base_nanos) return Period(ordinal=ordinal, freq=self.freq) raise IncompatibleFrequency("Input cannot be converted to " f"Period(freq={self.freqstr})") @@ -1581,14 +1632,10 @@ cdef class _Period(PeriodMixin): cdef: int64_t ordinal - if other.base == self.freq.base: - ordinal = self.ordinal + other.n - return Period(ordinal=ordinal, freq=self.freq) + self._require_matching_freq(other, base=True) - msg = DIFFERENT_FREQ.format(cls=type(self).__name__, - own_freq=self.freqstr, - other_freq=other.freqstr) - raise IncompatibleFrequency(msg) + ordinal = self.ordinal + other.n + return Period(ordinal=ordinal, freq=self.freq) def __add__(self, other): if not is_period_object(self): @@ -1598,7 +1645,7 @@ cdef class _Period(PeriodMixin): return other.__add__(self) if is_any_td_scalar(other): - return self._add_delta(other) + return self._add_timedeltalike_scalar(other) elif is_offset_object(other): return self._add_offset(other) elif other is NaT: @@ -1635,11 +1682,7 @@ cdef class _Period(PeriodMixin): ordinal = self.ordinal - other * self.freq.n return Period(ordinal=ordinal, freq=self.freq) elif is_period_object(other): - if other.freq != self.freq: - msg = DIFFERENT_FREQ.format(cls=type(self).__name__, - own_freq=self.freqstr, - other_freq=other.freqstr) - raise IncompatibleFrequency(msg) + self._require_matching_freq(other) # GH 23915 - mul by base freq since __add__ is agnostic of n return (self.ordinal - other.ordinal) * self.freq.base elif other is NaT: @@ -1677,40 +1720,6 @@ cdef class _Period(PeriodMixin): return Period(ordinal=ordinal, freq=freq) - @property - def start_time(self) -> Timestamp: - """ - Get the Timestamp for the start of the period. - - Returns - ------- - Timestamp - - See Also - -------- - Period.end_time : Return the end Timestamp. - Period.dayofyear : Return the day of year. - Period.daysinmonth : Return the days in that month. - Period.dayofweek : Return the day of the week. - - Examples - -------- - >>> period = pd.Period('2012-1-1', freq='D') - >>> period - Period('2012-01-01', 'D') - - >>> period.start_time - Timestamp('2012-01-01 00:00:00') - - >>> period.end_time - Timestamp('2012-01-01 23:59:59.999999999') - """ - return self.to_timestamp(how='S') - - @property - def end_time(self) -> Timestamp: - return self.to_timestamp(how="end") - def to_timestamp(self, freq=None, how='start', tz=None) -> Timestamp: """ Return the Timestamp representation of the Period. diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 749ec0a2b8848..8ffc41f692559 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -289,8 +289,7 @@ def _scalar_from_string(self, value: str) -> Period: def _check_compatible_with(self, other, setitem: bool = False): if other is NaT: return - if self.freqstr != other.freqstr: - raise raise_on_incompatible(self, other) + self._require_matching_freq(other) # -------------------------------------------------------------------- # Data / Attributes @@ -425,14 +424,6 @@ def is_leap_year(self) -> np.ndarray: """ return isleapyear_arr(np.asarray(self.year)) - @property - def start_time(self): - return self.to_timestamp(how="start") - - @property - def end_time(self): - return self.to_timestamp(how="end") - def to_timestamp(self, freq=None, how="start"): """ Cast to DatetimeArray/Index. @@ -659,11 +650,7 @@ def _sub_period_array(self, other): result : np.ndarray[object] Array of DateOffset objects; nulls represented by NaT. """ - if self.freq != other.freq: - msg = DIFFERENT_FREQ.format( - cls=type(self).__name__, own_freq=self.freqstr, other_freq=other.freqstr - ) - raise IncompatibleFrequency(msg) + self._require_matching_freq(other) new_values = algos.checked_add_with_arr( self.asi8, -other.asi8, arr_mask=self._isnan, b_mask=other._isnan @@ -702,8 +689,7 @@ def _addsub_int_array( def _add_offset(self, other: BaseOffset): assert not isinstance(other, Tick) - if other.base != self.freq.base: - raise raise_on_incompatible(self, other) + self._require_matching_freq(other, base=True) # Note: when calling parent class's _add_timedeltalike_scalar, # it will call delta_to_nanoseconds(delta). Because delta here diff --git a/pandas/core/indexes/extension.py b/pandas/core/indexes/extension.py index 2c4f40d5275e1..b25393ca3c58d 100644 --- a/pandas/core/indexes/extension.py +++ b/pandas/core/indexes/extension.py @@ -42,7 +42,8 @@ def inherit_from_data(name: str, delegate, cache: bool = False, wrap: bool = Fal """ attr = getattr(delegate, name) - if isinstance(attr, property): + if isinstance(attr, property) or type(attr).__name__ == "getset_descriptor": + # getset_descriptor i.e. property defined in cython class if cache: def cached(self):