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