diff --git a/doc/source/whatsnew/v2.0.1.rst b/doc/source/whatsnew/v2.0.1.rst index 023cb68300433..86d977730ab82 100644 --- a/doc/source/whatsnew/v2.0.1.rst +++ b/doc/source/whatsnew/v2.0.1.rst @@ -46,6 +46,7 @@ Other - :class:`DataFrame` created from empty dicts had :attr:`~DataFrame.columns` of dtype ``object``. It is now a :class:`RangeIndex` (:issue:`52404`) - :class:`Series` created from empty dicts had :attr:`~Series.index` of dtype ``object``. It is now a :class:`RangeIndex` (:issue:`52404`) - Implemented :meth:`Series.str.split` and :meth:`Series.str.rsplit` for :class:`ArrowDtype` with ``pyarrow.string`` (:issue:`52401`) +- Implemented :meth:`_Timestamp._ensure_components` and :meth:`_Timestamp.components` timestamps.pyx and :meth:`Series.dt.components` in datetimes.py .. --------------------------------------------------------------------------- .. _whatsnew_201.contributors: diff --git a/pandas/_libs/tslibs/timestamps.pxd b/pandas/_libs/tslibs/timestamps.pxd index 26018cd904249..9a885f8fcc437 100644 --- a/pandas/_libs/tslibs/timestamps.pxd +++ b/pandas/_libs/tslibs/timestamps.pxd @@ -23,6 +23,8 @@ cdef class _Timestamp(ABCTimestamp): cdef readonly: int64_t _value, nanosecond, year NPY_DATETIMEUNIT _creso + bint _is_populated # are my components populated + int64_t _y, _month, _d, _h, _m, _s, _us, _ns cdef bint _get_start_end_field(self, str field, freq) cdef _get_date_name_field(self, str field, object locale) @@ -34,3 +36,4 @@ cdef class _Timestamp(ABCTimestamp): int op) except -1 cdef bint _compare_mismatched_resos(_Timestamp self, _Timestamp other, int op) cdef _Timestamp _as_creso(_Timestamp self, NPY_DATETIMEUNIT creso, bint round_ok=*) + cdef _ensure_components(_Timestamp self) diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index fd89d0e795ecc..0576274cff3fa 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -7,6 +7,7 @@ construction requirements, we need to do object instantiation in python shadows the python class, where we do any heavy lifting. """ +import collections import warnings cimport cython @@ -128,6 +129,21 @@ from pandas._libs.tslibs.tzconversion cimport ( _zero_time = dt_time(0, 0) _no_input = object() +# components named tuple +Components = collections.namedtuple( + "Components", + [ + "year", + "month", + "day", + "hour", + "minute", + "second", + "microsecond", + "nanosecond", + ] +) + # ---------------------------------------------------------------------- @@ -227,7 +243,6 @@ class MinMaxReso: # ---------------------------------------------------------------------- cdef class _Timestamp(ABCTimestamp): - # higher than np.ndarray and np.matrix __array_priority__ = 100 dayofweek = _Timestamp.day_of_week @@ -568,6 +583,37 @@ cdef class _Timestamp(ABCTimestamp): return type(self)(other) - self return NotImplemented + cdef _ensure_components(_Timestamp self): + """ + compute the components + """ + + if self._is_populated: + return + + cdef: + npy_datetimestruct dts + + pandas_datetime_to_datetimestruct(self._value, self._creso, &dts) + self._y = dts.year + self._month = dts.month + self._d = dts.day + self._h = dts.hour + self._m = dts.min + self._s = dts.sec + self._us = dts.us + self._ns = dts.ps // 1000 + + self._is_populated = 1 + + @property + def components(self): + self._ensure_components() + return Components( + self._y, self._month, self._d, + self._h, self._m, self._s, self._us, self._ns + ) + # ----------------------------------------------------------------- cdef int64_t _maybe_convert_value_to_local(self): diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 582a043a8a78a..77ebcb4b3fed9 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -9145,9 +9145,19 @@ def last(self, offset) -> Self: offset = to_offset(offset) - start_date = self.index[-1] - offset - start = self.index.searchsorted(start_date, side="right") - return self.iloc[start:] + if not isinstance(offset, Tick) and offset.is_on_offset(self.index[-1]): + # GH#29623 if first value is end of period, remove offset with n = 1 + # before adding the real offset + start_date = start = self.index[-1] - offset.base - offset + else: + start_date = start = self.index[-1] - offset + + # Tick-like, e.g. 3 weeks + if isinstance(offset, Tick) and start_date in self.index: + start = self.index.searchsorted(start_date, side="right") + return self.iloc[:start] + + return self.loc[:start] @final def rank( diff --git a/pandas/tests/scalar/test_nat.py b/pandas/tests/scalar/test_nat.py index c13ea4eeb9e0d..0accbbf84f3f3 100644 --- a/pandas/tests/scalar/test_nat.py +++ b/pandas/tests/scalar/test_nat.py @@ -174,7 +174,7 @@ def test_nat_iso_format(get_nat): @pytest.mark.parametrize( "klass,expected", [ - (Timestamp, ["normalize", "to_julian_date", "to_period", "unit"]), + (Timestamp, ["components", "normalize", "to_julian_date", "to_period", "unit"]), ( Timedelta, [ diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py index afb4dd7422114..40520be9dcea0 100644 --- a/pandas/tests/scalar/timestamp/test_timestamp.py +++ b/pandas/tests/scalar/timestamp/test_timestamp.py @@ -94,6 +94,40 @@ def test_fields(self, attr, expected, tz): assert isinstance(result, int) assert result == expected + # components + tzstr = "dateutil/usr/share/zoneinfo/America/Chicago" + ts = Timestamp( + year=2013, + month=11, + day=3, + hour=1, + minute=0, + fold=1, + second=32, + microsecond=3, + nanosecond=7, + tz=tzstr, + ).components + + assert ts.year == 2013 + assert ts.month == 11 + assert ts.day == 3 + assert ts.hour == 1 + assert ts.minute == 0 + assert ts.second == 32 + assert ts.microsecond == 3 + assert ts.nanosecond == 7 + + tzstr = "dateutil/usr/share/zoneinfo/America/Detroit" + ts = Timestamp( + year=2023, month=4, day=14, hour=9, minute=53, fold=1, tz=tzstr + ).components + assert ts.year == 2023 + assert ts.month == 4 + assert ts.day == 14 + assert ts.hour == 9 + assert ts.minute == 53 + @pytest.mark.parametrize("tz", [None, "US/Eastern"]) def test_millisecond_raises(self, tz): ts = Timestamp("2014-12-31 23:59:00", tz=tz)