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