1"""
2datetimelike delegation
3"""
4
5from __future__ import annotations
6
7from typing import (
8 TYPE_CHECKING,
9 NoReturn,
10 cast,
11)
12import warnings
13
14import numpy as np
15
16from pandas._libs import lib
17from pandas.errors import Pandas4Warning
18from pandas.util._exceptions import find_stack_level
19
20from pandas.core.dtypes.common import (
21 is_integer_dtype,
22 is_list_like,
23)
24from pandas.core.dtypes.dtypes import (
25 ArrowDtype,
26 CategoricalDtype,
27 DatetimeTZDtype,
28 PeriodDtype,
29)
30from pandas.core.dtypes.generic import ABCSeries
31
32from pandas.core.accessor import (
33 PandasDelegate,
34 delegate_names,
35)
36from pandas.core.arrays import (
37 DatetimeArray,
38 PeriodArray,
39 TimedeltaArray,
40)
41from pandas.core.arrays.arrow.array import ArrowExtensionArray
42from pandas.core.base import (
43 NoNewAttributesMixin,
44 PandasObject,
45)
46from pandas.core.indexes.datetimes import DatetimeIndex
47from pandas.core.indexes.timedeltas import TimedeltaIndex
48
49if TYPE_CHECKING:
50 from pandas import (
51 DataFrame,
52 Series,
53 )
54
55
56class Properties(PandasDelegate, PandasObject, NoNewAttributesMixin):
57 _hidden_attrs = PandasObject._hidden_attrs | {
58 "orig",
59 "name",
60 }
61
62 def __init__(self, data: Series, orig) -> None:
63 if not isinstance(data, ABCSeries):
64 raise TypeError(
65 f"cannot convert an object of type {type(data)} to a datetimelike index"
66 )
67
68 self._parent = data
69 self.orig = orig
70 self.name = getattr(data, "name", None)
71 self._freeze()
72
73 def _get_values(self):
74 data = self._parent
75 if lib.is_np_dtype(data.dtype, "M"):
76 return DatetimeIndex(data, copy=False, name=self.name)
77
78 elif isinstance(data.dtype, DatetimeTZDtype):
79 return DatetimeIndex(data, copy=False, name=self.name)
80
81 elif lib.is_np_dtype(data.dtype, "m"):
82 return TimedeltaIndex(data, copy=False, name=self.name)
83
84 elif isinstance(data.dtype, PeriodDtype):
85 return PeriodArray(data, copy=False)
86
87 raise TypeError(
88 f"cannot convert an object of type {type(data)} to a datetimelike index"
89 )
90
91 def _delegate_property_get(self, name: str):
92 from pandas import Series
93
94 values = self._get_values()
95
96 result = getattr(values, name)
97
98 # maybe need to upcast (ints)
99 if isinstance(result, np.ndarray):
100 if is_integer_dtype(result):
101 result = result.astype("int64")
102 elif not is_list_like(result):
103 return result
104
105 result = np.asarray(result)
106
107 if self.orig is not None:
108 index = self.orig.index
109 else:
110 index = self._parent.index
111 # return the result as a Series
112 return Series(result, index=index, name=self.name).__finalize__(self._parent)
113
114 def _delegate_property_set(self, name: str, value, *args, **kwargs) -> NoReturn:
115 raise ValueError(
116 "modifications to a property of a datetimelike object are not supported. "
117 "Change values on the original."
118 )
119
120 def _delegate_method(self, name: str, *args, **kwargs):
121 from pandas import Series
122
123 values = self._get_values()
124
125 method = getattr(values, name)
126 result = method(*args, **kwargs)
127
128 if not is_list_like(result):
129 return result
130
131 return Series(result, index=self._parent.index, name=self.name).__finalize__(
132 self._parent
133 )
134
135
136@delegate_names(
137 delegate=ArrowExtensionArray,
138 accessors=TimedeltaArray._datetimelike_ops,
139 typ="property",
140 accessor_mapping=lambda x: f"_dt_{x}",
141 raise_on_missing=False,
142)
143@delegate_names(
144 delegate=ArrowExtensionArray,
145 accessors=TimedeltaArray._datetimelike_methods,
146 typ="method",
147 accessor_mapping=lambda x: f"_dt_{x}",
148 raise_on_missing=False,
149)
150@delegate_names(
151 delegate=ArrowExtensionArray,
152 accessors=DatetimeArray._datetimelike_ops,
153 typ="property",
154 accessor_mapping=lambda x: f"_dt_{x}",
155 raise_on_missing=False,
156)
157@delegate_names(
158 delegate=ArrowExtensionArray,
159 accessors=DatetimeArray._datetimelike_methods,
160 typ="method",
161 accessor_mapping=lambda x: f"_dt_{x}",
162 raise_on_missing=False,
163)
164class ArrowTemporalProperties(PandasDelegate, PandasObject, NoNewAttributesMixin):
165 def __init__(self, data: Series, orig) -> None:
166 if not isinstance(data, ABCSeries):
167 raise TypeError(
168 f"cannot convert an object of type {type(data)} to a datetimelike index"
169 )
170
171 self._parent = data
172 self._orig = orig
173 self._freeze()
174
175 def _delegate_property_get(self, name: str):
176 if not hasattr(self._parent.array, f"_dt_{name}"):
177 raise NotImplementedError(
178 f"dt.{name} is not supported for {self._parent.dtype}"
179 )
180 result = getattr(self._parent.array, f"_dt_{name}")
181
182 if not is_list_like(result):
183 return result
184
185 if self._orig is not None:
186 index = self._orig.index
187 else:
188 index = self._parent.index
189 # return the result as a Series, which is by definition a copy
190 result = type(self._parent)(
191 result, index=index, name=self._parent.name
192 ).__finalize__(self._parent)
193
194 return result
195
196 def _delegate_method(self, name: str, *args, **kwargs):
197 if not hasattr(self._parent.array, f"_dt_{name}"):
198 raise NotImplementedError(
199 f"dt.{name} is not supported for {self._parent.dtype}"
200 )
201
202 result = getattr(self._parent.array, f"_dt_{name}")(*args, **kwargs)
203
204 if self._orig is not None:
205 index = self._orig.index
206 else:
207 index = self._parent.index
208 # return the result as a Series, which is by definition a copy
209 result = type(self._parent)(
210 result, index=index, name=self._parent.name
211 ).__finalize__(self._parent)
212
213 return result
214
215 def to_pytimedelta(self):
216 # GH 57463
217 warnings.warn(
218 f"The behavior of {type(self).__name__}.to_pytimedelta is deprecated, "
219 "in a future version this will return a Series containing python "
220 "datetime.timedelta objects instead of an ndarray. To retain the "
221 "old behavior, call `np.array` on the result",
222 Pandas4Warning,
223 stacklevel=find_stack_level(),
224 )
225 return cast(ArrowExtensionArray, self._parent.array)._dt_to_pytimedelta()
226
227 def to_pydatetime(self) -> Series:
228 # GH#20306
229 return cast(ArrowExtensionArray, self._parent.array)._dt_to_pydatetime()
230
231 def isocalendar(self) -> DataFrame:
232 from pandas import DataFrame
233
234 result = (
235 cast(ArrowExtensionArray, self._parent.array)
236 ._dt_isocalendar()
237 ._pa_array.combine_chunks()
238 )
239 iso_calendar_df = DataFrame(
240 {
241 col: type(self._parent.array)(result.field(i)) # type: ignore[call-arg]
242 for i, col in enumerate(["year", "week", "day"])
243 }
244 )
245 return iso_calendar_df
246
247 @property
248 def components(self) -> DataFrame:
249 from pandas import DataFrame
250
251 components_df = DataFrame(
252 {
253 col: getattr(self._parent.array, f"_dt_{col}")
254 for col in [
255 "days",
256 "hours",
257 "minutes",
258 "seconds",
259 "milliseconds",
260 "microseconds",
261 "nanoseconds",
262 ]
263 }
264 )
265 return components_df
266
267
268@delegate_names(
269 delegate=DatetimeArray,
270 accessors=[*DatetimeArray._datetimelike_ops, "unit"],
271 typ="property",
272)
273@delegate_names(
274 delegate=DatetimeArray,
275 accessors=[*DatetimeArray._datetimelike_methods, "as_unit"],
276 typ="method",
277)
278class DatetimeProperties(Properties):
279 """
280 Accessor object for datetimelike properties of the Series values.
281
282 Examples
283 --------
284 >>> seconds_series = pd.Series(pd.date_range("2000-01-01", periods=3, freq="s"))
285 >>> seconds_series
286 0 2000-01-01 00:00:00
287 1 2000-01-01 00:00:01
288 2 2000-01-01 00:00:02
289 dtype: datetime64[us]
290 >>> seconds_series.dt.second
291 0 0
292 1 1
293 2 2
294 dtype: int32
295
296 >>> hours_series = pd.Series(pd.date_range("2000-01-01", periods=3, freq="h"))
297 >>> hours_series
298 0 2000-01-01 00:00:00
299 1 2000-01-01 01:00:00
300 2 2000-01-01 02:00:00
301 dtype: datetime64[us]
302 >>> hours_series.dt.hour
303 0 0
304 1 1
305 2 2
306 dtype: int32
307
308 >>> quarters_series = pd.Series(pd.date_range("2000-01-01", periods=3, freq="QE"))
309 >>> quarters_series
310 0 2000-03-31
311 1 2000-06-30
312 2 2000-09-30
313 dtype: datetime64[us]
314 >>> quarters_series.dt.quarter
315 0 1
316 1 2
317 2 3
318 dtype: int32
319
320 Returns a Series indexed like the original Series.
321 Raises TypeError if the Series does not contain datetimelike values.
322 """
323
324 def to_pydatetime(self) -> Series:
325 """
326 Return the data as a Series of :class:`datetime.datetime` objects.
327
328 Timezone information is retained if present.
329
330 .. warning::
331
332 Python's datetime uses microsecond resolution, which is lower than
333 pandas (nanosecond). The values are truncated.
334
335 Returns
336 -------
337 numpy.ndarray
338 Object dtype array containing native Python datetime objects.
339
340 See Also
341 --------
342 datetime.datetime : Standard library value for a datetime.
343
344 Examples
345 --------
346 >>> s = pd.Series(pd.date_range("20180310", periods=2))
347 >>> s
348 0 2018-03-10
349 1 2018-03-11
350 dtype: datetime64[us]
351
352 >>> s.dt.to_pydatetime()
353 0 2018-03-10 00:00:00
354 1 2018-03-11 00:00:00
355 dtype: object
356
357 pandas' nanosecond precision is truncated to microseconds.
358
359 >>> s = pd.Series(pd.date_range("20180310", periods=2, freq="ns"))
360 >>> s
361 0 2018-03-10 00:00:00.000000000
362 1 2018-03-10 00:00:00.000000001
363 dtype: datetime64[ns]
364
365 >>> s.dt.to_pydatetime()
366 0 2018-03-10 00:00:00
367 1 2018-03-10 00:00:00
368 dtype: object
369 """
370 # GH#20306
371 from pandas import Series
372
373 return Series(self._get_values().to_pydatetime(), dtype=object)
374
375 @property
376 def freq(self):
377 """
378 Tries to return a string representing a frequency generated by infer_freq.
379
380 Returns None if it can't autodetect the frequency.
381
382 See Also
383 --------
384 Series.dt.to_period : Cast to PeriodArray/PeriodIndex at a particular
385 frequency.
386
387 Examples
388 --------
389 >>> ser = pd.Series(["2024-01-01", "2024-01-02", "2024-01-03", "2024-01-04"])
390 >>> ser = pd.to_datetime(ser)
391 >>> ser.dt.freq
392 'D'
393
394 >>> ser = pd.Series(["2022-01-01", "2024-01-01", "2026-01-01", "2028-01-01"])
395 >>> ser = pd.to_datetime(ser)
396 >>> ser.dt.freq
397 '2YS-JAN'
398 """
399 return self._get_values().inferred_freq
400
401 def isocalendar(self) -> DataFrame:
402 """
403 Calculate year, week, and day according to the ISO 8601 standard.
404
405 Returns
406 -------
407 DataFrame
408 With columns year, week and day.
409
410 See Also
411 --------
412 Timestamp.isocalendar : Function return a 3-tuple containing ISO year,
413 week number, and weekday for the given Timestamp object.
414 datetime.date.isocalendar : Return a named tuple object with
415 three components: year, week and weekday.
416
417 Examples
418 --------
419 >>> ser = pd.to_datetime(pd.Series(["2010-01-01", pd.NaT]))
420 >>> ser.dt.isocalendar()
421 year week day
422 0 2009 53 5
423 1 <NA> <NA> <NA>
424 >>> ser.dt.isocalendar().week
425 0 53
426 1 <NA>
427 Name: week, dtype: UInt32
428 """
429 return self._get_values().isocalendar().set_index(self._parent.index)
430
431
432@delegate_names(
433 delegate=TimedeltaArray, accessors=TimedeltaArray._datetimelike_ops, typ="property"
434)
435@delegate_names(
436 delegate=TimedeltaArray,
437 accessors=TimedeltaArray._datetimelike_methods,
438 typ="method",
439)
440class TimedeltaProperties(Properties):
441 """
442 Accessor object for datetimelike properties of the Series values.
443
444 Returns a Series indexed like the original Series.
445 Raises TypeError if the Series does not contain datetimelike values.
446
447 Examples
448 --------
449 >>> seconds_series = pd.Series(
450 ... pd.timedelta_range(start="1 second", periods=3, freq="s")
451 ... )
452 >>> seconds_series
453 0 0 days 00:00:01
454 1 0 days 00:00:02
455 2 0 days 00:00:03
456 dtype: timedelta64[us]
457 >>> seconds_series.dt.seconds
458 0 1
459 1 2
460 2 3
461 dtype: int32
462 """
463
464 def to_pytimedelta(self) -> np.ndarray:
465 """
466 Return an array of native :class:`datetime.timedelta` objects.
467
468 Python's standard `datetime` library uses a different representation
469 timedelta's. This method converts a Series of pandas Timedeltas
470 to `datetime.timedelta` format with the same length as the original
471 Series.
472
473 Returns
474 -------
475 numpy.ndarray
476 Array of 1D containing data with `datetime.timedelta` type.
477
478 See Also
479 --------
480 datetime.timedelta : A duration expressing the difference
481 between two date, time, or datetime.
482
483 Examples
484 --------
485 >>> s = pd.Series(pd.to_timedelta(np.arange(5), unit="D"))
486 >>> s
487 0 0 days
488 1 1 days
489 2 2 days
490 3 3 days
491 4 4 days
492 dtype: timedelta64[s]
493
494 >>> s.dt.to_pytimedelta()
495 array([datetime.timedelta(0), datetime.timedelta(days=1),
496 datetime.timedelta(days=2), datetime.timedelta(days=3),
497 datetime.timedelta(days=4)], dtype=object)
498 """
499 # GH 57463
500 warnings.warn(
501 f"The behavior of {type(self).__name__}.to_pytimedelta is deprecated, "
502 "in a future version this will return a Series containing python "
503 "datetime.timedelta objects instead of an ndarray. To retain the "
504 "old behavior, call `np.array` on the result",
505 Pandas4Warning,
506 stacklevel=find_stack_level(),
507 )
508 return self._get_values().to_pytimedelta()
509
510 @property
511 def components(self) -> DataFrame:
512 """
513 Return a Dataframe of the components of the Timedeltas.
514
515 Each row of the DataFrame corresponds to a Timedelta in the original
516 Series and contains the individual components (days, hours, minutes,
517 seconds, milliseconds, microseconds, nanoseconds) of the Timedelta.
518
519 Returns
520 -------
521 DataFrame
522
523 See Also
524 --------
525 TimedeltaIndex.components : Return a DataFrame of the individual resolution
526 components of the Timedeltas.
527 Series.dt.total_seconds : Return the total number of seconds in the duration.
528
529 Examples
530 --------
531 >>> s = pd.Series(pd.to_timedelta(np.arange(5), unit="s"))
532 >>> s
533 0 0 days 00:00:00
534 1 0 days 00:00:01
535 2 0 days 00:00:02
536 3 0 days 00:00:03
537 4 0 days 00:00:04
538 dtype: timedelta64[s]
539 >>> s.dt.components
540 days hours minutes seconds milliseconds microseconds nanoseconds
541 0 0 0 0 0 0 0 0
542 1 0 0 0 1 0 0 0
543 2 0 0 0 2 0 0 0
544 3 0 0 0 3 0 0 0
545 4 0 0 0 4 0 0 0
546 """
547 return (
548 self._get_values()
549 .components.set_index(self._parent.index)
550 .__finalize__(self._parent)
551 )
552
553 @property
554 def freq(self):
555 return self._get_values().inferred_freq
556
557
558@delegate_names(
559 delegate=PeriodArray, accessors=PeriodArray._datetimelike_ops, typ="property"
560)
561@delegate_names(
562 delegate=PeriodArray, accessors=PeriodArray._datetimelike_methods, typ="method"
563)
564class PeriodProperties(Properties):
565 """
566 Accessor object for datetimelike properties of the Series values.
567
568 Returns a Series indexed like the original Series.
569 Raises TypeError if the Series does not contain datetimelike values.
570
571 Examples
572 --------
573 >>> seconds_series = pd.Series(
574 ... pd.period_range(
575 ... start="2000-01-01 00:00:00", end="2000-01-01 00:00:03", freq="s"
576 ... )
577 ... )
578 >>> seconds_series
579 0 2000-01-01 00:00:00
580 1 2000-01-01 00:00:01
581 2 2000-01-01 00:00:02
582 3 2000-01-01 00:00:03
583 dtype: period[s]
584 >>> seconds_series.dt.second
585 0 0
586 1 1
587 2 2
588 3 3
589 dtype: int64
590
591 >>> hours_series = pd.Series(
592 ... pd.period_range(start="2000-01-01 00:00", end="2000-01-01 03:00", freq="h")
593 ... )
594 >>> hours_series
595 0 2000-01-01 00:00
596 1 2000-01-01 01:00
597 2 2000-01-01 02:00
598 3 2000-01-01 03:00
599 dtype: period[h]
600 >>> hours_series.dt.hour
601 0 0
602 1 1
603 2 2
604 3 3
605 dtype: int64
606
607 >>> quarters_series = pd.Series(
608 ... pd.period_range(start="2000-01-01", end="2000-12-31", freq="Q-DEC")
609 ... )
610 >>> quarters_series
611 0 2000Q1
612 1 2000Q2
613 2 2000Q3
614 3 2000Q4
615 dtype: period[Q-DEC]
616 >>> quarters_series.dt.quarter
617 0 1
618 1 2
619 2 3
620 3 4
621 dtype: int64
622 """
623
624
625class CombinedDatetimelikeProperties(
626 DatetimeProperties, TimedeltaProperties, PeriodProperties
627):
628 """
629 Accessor object for Series values' datetime-like, timedelta and period properties.
630
631 See Also
632 --------
633 DatetimeIndex : Index of datetime64 data.
634
635 Examples
636 --------
637 >>> dates = pd.Series(
638 ... ["2024-01-01", "2024-01-15", "2024-02-5"], dtype="datetime64[ns]"
639 ... )
640 >>> dates.dt.day
641 0 1
642 1 15
643 2 5
644 dtype: int32
645 >>> dates.dt.month
646 0 1
647 1 1
648 2 2
649 dtype: int32
650
651 >>> dates = pd.Series(
652 ... ["2024-01-01", "2024-01-15", "2024-02-5"], dtype="datetime64[ns, UTC]"
653 ... )
654 >>> dates.dt.day
655 0 1
656 1 15
657 2 5
658 dtype: int32
659 >>> dates.dt.month
660 0 1
661 1 1
662 2 2
663 dtype: int32
664 """
665
666 def __new__(cls, data: Series): # pyright: ignore[reportInconsistentConstructor]
667 # CombinedDatetimelikeProperties isn't really instantiated. Instead
668 # we need to choose which parent (datetime or timedelta) is
669 # appropriate. Since we're checking the dtypes anyway, we'll just
670 # do all the validation here.
671
672 if not isinstance(data, ABCSeries):
673 raise TypeError(
674 f"cannot convert an object of type {type(data)} to a datetimelike index"
675 )
676
677 orig = data if isinstance(data.dtype, CategoricalDtype) else None
678 if orig is not None:
679 data = data._constructor(
680 orig.array,
681 name=orig.name,
682 copy=False,
683 dtype=orig._values.categories.dtype,
684 index=orig.index,
685 )
686
687 if isinstance(data.dtype, ArrowDtype) and data.dtype.kind in "Mm":
688 return ArrowTemporalProperties(data, orig)
689 if lib.is_np_dtype(data.dtype, "M"):
690 return DatetimeProperties(data, orig)
691 elif isinstance(data.dtype, DatetimeTZDtype):
692 return DatetimeProperties(data, orig)
693 elif lib.is_np_dtype(data.dtype, "m"):
694 return TimedeltaProperties(data, orig)
695 elif isinstance(data.dtype, PeriodDtype):
696 return PeriodProperties(data, orig)
697
698 raise AttributeError("Can only use .dt accessor with datetimelike values")