1from __future__ import annotations
2
3from datetime import (
4 datetime,
5 timedelta,
6 tzinfo,
7)
8from typing import (
9 TYPE_CHECKING,
10 Self,
11 TypeVar,
12 cast,
13 overload,
14)
15import warnings
16
17import numpy as np
18
19from pandas._config import using_string_dtype
20from pandas._config.config import _global_config as config
21
22from pandas._libs import (
23 lib,
24 tslib,
25)
26from pandas._libs.tslibs import (
27 BaseOffset,
28 NaT,
29 NaTType,
30 OutOfBoundsDatetime,
31 OutOfBoundsTimedelta,
32 Timedelta,
33 Timestamp,
34 astype_overflowsafe,
35 fields,
36 get_supported_dtype,
37 get_unit_from_dtype,
38 ints_to_pydatetime,
39 is_date_array_normalized,
40 is_supported_dtype,
41 is_unitless,
42 normalize_i8_timestamps,
43 timezones,
44 to_offset,
45 tz_convert_from_utc,
46 tzconversion,
47)
48from pandas._libs.tslibs.ccalendar import (
49 DAYS_FULL,
50 MONTHS_FULL,
51)
52from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
53from pandas._libs.tslibs.offsets import RelativeDeltaOffset
54from pandas.compat.pyarrow import HAS_PYARROW
55from pandas.errors import (
56 Pandas4Warning,
57 PerformanceWarning,
58)
59from pandas.util._decorators import set_module
60from pandas.util._exceptions import find_stack_level
61from pandas.util._validators import validate_inclusive
62
63from pandas.core.dtypes.common import (
64 DT64NS_DTYPE,
65 INT64_DTYPE,
66 is_bool_dtype,
67 is_float_dtype,
68 is_string_dtype,
69 pandas_dtype,
70)
71from pandas.core.dtypes.dtypes import (
72 DatetimeTZDtype,
73 ExtensionDtype,
74 PeriodDtype,
75)
76
77from pandas.core.arrays import datetimelike as dtl
78from pandas.core.arrays._ranges import (
79 generate_daily_offset_range,
80 generate_regular_range,
81)
82import pandas.core.common as com
83
84from pandas.tseries.frequencies import get_period_alias
85from pandas.tseries.offsets import (
86 Day,
87 Tick,
88)
89
90if TYPE_CHECKING:
91 from collections.abc import (
92 Callable,
93 Generator,
94 )
95
96 import pyarrow as pa
97
98 from pandas._typing import (
99 ArrayLike,
100 DateTimeErrorChoices,
101 DtypeObj,
102 IntervalClosedType,
103 TimeAmbiguous,
104 TimeNonexistent,
105 TimeUnit,
106 npt,
107 )
108
109 from pandas import DataFrame
110 from pandas.core.arrays import PeriodArray
111 from pandas.core.arrays.string_arrow import ArrowStringArray
112
113 _TimestampNoneT1 = TypeVar("_TimestampNoneT1", Timestamp, None)
114 _TimestampNoneT2 = TypeVar("_TimestampNoneT2", Timestamp, None)
115
116
117@overload
118def tz_to_dtype(tz: tzinfo, unit: TimeUnit = ...) -> DatetimeTZDtype: ...
119
120
121@overload
122def tz_to_dtype(tz: None, unit: TimeUnit = ...) -> np.dtype[np.datetime64]: ...
123
124
125def tz_to_dtype(
126 tz: tzinfo | None, unit: TimeUnit = "ns"
127) -> np.dtype[np.datetime64] | DatetimeTZDtype:
128 """
129 Return a datetime64[ns] dtype appropriate for the given timezone.
130
131 Parameters
132 ----------
133 tz : tzinfo or None
134 unit : str, default "ns"
135
136 Returns
137 -------
138 np.dtype or DatetimeTZDtype
139 """
140 if tz is None:
141 return np.dtype(f"M8[{unit}]")
142 else:
143 return DatetimeTZDtype(tz=tz, unit=unit)
144
145
146def _field_accessor(name: str, field: str, docstring: str | None = None):
147 def f(self):
148 values = self._local_timestamps()
149
150 if field in self._bool_ops:
151 if field.endswith(("start", "end")):
152 return self._get_start_end_field(field, freq=None)
153 return fields.get_date_field(values, field, reso=self._creso)
154
155 result = fields.get_date_field(values, field, reso=self._creso)
156 result = self._maybe_mask_results(result, fill_value=None, convert="float64")
157
158 return result
159
160 f.__name__ = name
161 f.__doc__ = docstring
162 return property(f)
163
164
165@set_module("pandas.arrays")
166class DatetimeArray(dtl.TimelikeOps, dtl.DatelikeOps):
167 """
168 Pandas ExtensionArray for tz-naive or tz-aware datetime data.
169
170 .. warning::
171
172 DatetimeArray is currently experimental, and its API may change
173 without warning. In particular, :attr:`DatetimeArray.dtype` is
174 expected to change to always be an instance of an ``ExtensionDtype``
175 subclass.
176
177 Parameters
178 ----------
179 data : Series, Index, DatetimeArray, ndarray
180 The datetime data.
181
182 For DatetimeArray `values` (or a Series or Index boxing one),
183 `dtype` and `freq` will be extracted from `values`.
184
185 dtype : numpy.dtype or DatetimeTZDtype
186 Note that the only NumPy dtypes allowed are 'datetime64[ns]',
187 'datetime64[us]', 'datetime64[ms]', 'datetime64[s]'.
188 freq : str or Offset, optional
189 The frequency.
190 copy : bool, default False
191 Whether to copy the underlying array of values.
192
193 Attributes
194 ----------
195 None
196
197 Methods
198 -------
199 None
200
201 See Also
202 --------
203 DatetimeIndex : Immutable Index for datetime-like data.
204 Series : One-dimensional labeled array capable of holding datetime-like data.
205 Timestamp : Pandas replacement for python datetime.datetime object.
206 to_datetime : Convert argument to datetime.
207 period_range : Return a fixed frequency PeriodIndex.
208
209 Examples
210 --------
211 >>> pd.arrays.DatetimeArray._from_sequence(
212 ... pd.DatetimeIndex(["2023-01-01", "2023-01-02"], freq="D")
213 ... )
214 <DatetimeArray>
215 ['2023-01-01 00:00:00', '2023-01-02 00:00:00']
216 Length: 2, dtype: datetime64[us]
217 """
218
219 _typ = "datetimearray"
220 _recognized_scalars = (datetime, np.datetime64)
221 _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: (
222 lib.is_np_dtype(x, "M") or isinstance(x, DatetimeTZDtype)
223 )
224
225 @property
226 def _internal_fill_value(self) -> np.datetime64:
227 return np.datetime64("NaT", self.unit)
228
229 @property
230 def _scalar_type(self) -> type[Timestamp]:
231 return Timestamp
232
233 # define my properties & methods for delegation
234 _bool_ops: list[str] = [
235 "is_month_start",
236 "is_month_end",
237 "is_quarter_start",
238 "is_quarter_end",
239 "is_year_start",
240 "is_year_end",
241 "is_leap_year",
242 ]
243 _field_ops: list[str] = [
244 "year",
245 "month",
246 "day",
247 "hour",
248 "minute",
249 "second",
250 "day_of_week",
251 "day_of_year",
252 "quarter",
253 "days_in_month",
254 "microsecond",
255 "nanosecond",
256 ]
257 _other_ops: list[str] = ["date", "time", "timetz"]
258 _datetimelike_ops: list[str] = _field_ops + _bool_ops + _other_ops + ["unit", "tz"]
259 _datetimelike_methods: list[str] = [
260 "to_period",
261 "tz_localize",
262 "tz_convert",
263 "normalize",
264 "strftime",
265 "round",
266 "floor",
267 "ceil",
268 "month_name",
269 "day_name",
270 "as_unit",
271 ]
272
273 # ndim is inherited from ExtensionArray, must exist to ensure
274 # Timestamp.__richcmp__(DateTimeArray) operates pointwise
275
276 # ensure that operations with numpy arrays defer to our implementation
277 __array_priority__ = 1000
278
279 # -----------------------------------------------------------------
280 # Constructors
281
282 _dtype: np.dtype[np.datetime64] | DatetimeTZDtype
283
284 @classmethod
285 def _validate_dtype(cls, values, dtype):
286 # used in TimeLikeOps.__init__
287 dtype = _validate_dt64_dtype(dtype)
288 _validate_dt64_dtype(values.dtype)
289 if isinstance(dtype, np.dtype):
290 if values.dtype != dtype:
291 raise ValueError("Values resolution does not match dtype.")
292 else:
293 vunit = np.datetime_data(values.dtype)[0]
294 if vunit != dtype.unit:
295 raise ValueError("Values resolution does not match dtype.")
296 return dtype
297
298 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked"
299 @classmethod
300 def _simple_new( # type: ignore[override]
301 cls,
302 values: npt.NDArray[np.datetime64],
303 dtype: np.dtype[np.datetime64] | DatetimeTZDtype = DT64NS_DTYPE,
304 ) -> Self:
305 assert isinstance(values, np.ndarray)
306 assert dtype.kind == "M"
307 if isinstance(dtype, np.dtype):
308 assert dtype == values.dtype
309 assert not is_unitless(dtype)
310 else:
311 # DatetimeTZDtype. If we have e.g. DatetimeTZDtype[us, UTC],
312 # then values.dtype should be M8[us].
313 assert dtype._creso == get_unit_from_dtype(values.dtype)
314
315 return super()._simple_new(values, dtype)
316
317 @classmethod
318 def _from_sequence(cls, scalars, *, dtype=None, copy: bool = False) -> Self:
319 return cls._from_sequence_not_strict(scalars, dtype=dtype, copy=copy)
320
321 @classmethod
322 def _from_sequence_not_strict(
323 cls,
324 data,
325 *,
326 dtype=None,
327 copy: bool = False,
328 tz=lib.no_default,
329 dayfirst: bool = False,
330 yearfirst: bool = False,
331 ambiguous: TimeAmbiguous = "raise",
332 ) -> Self:
333 """
334 A non-strict version of _from_sequence, called from DatetimeIndex.__new__.
335 """
336
337 # if the user either explicitly passes tz=None or a tz-naive dtype, we
338 # disallow inferring a tz.
339 explicit_tz_none = tz is None
340 if tz is lib.no_default:
341 tz = None
342 else:
343 tz = timezones.maybe_get_tz(tz)
344
345 dtype = _validate_dt64_dtype(dtype)
346 # if dtype has an embedded tz, capture it
347 tz = _validate_tz_from_dtype(dtype, tz, explicit_tz_none)
348
349 unit = None
350 if dtype is not None:
351 unit = dtl.dtype_to_unit(dtype)
352
353 data, copy = dtl.ensure_arraylike_for_datetimelike(
354 data, copy, cls_name="DatetimeArray"
355 )
356
357 subarr, tz = _sequence_to_dt64(
358 data,
359 copy=copy,
360 tz=tz,
361 dayfirst=dayfirst,
362 yearfirst=yearfirst,
363 ambiguous=ambiguous,
364 out_unit=unit,
365 )
366 # We have to call this again after possibly inferring a tz above
367 _validate_tz_from_dtype(dtype, tz, explicit_tz_none)
368 if tz is not None and explicit_tz_none:
369 raise ValueError(
370 "Passed data is timezone-aware, incompatible with 'tz=None'. "
371 "Use obj.tz_localize(None) instead."
372 )
373
374 data_unit = np.datetime_data(subarr.dtype)[0]
375 data_unit = cast("TimeUnit", data_unit)
376 data_dtype = tz_to_dtype(tz, data_unit)
377 result = cls._simple_new(subarr, dtype=data_dtype)
378 if unit is not None and unit != result.unit:
379 # If unit was specified in user-passed dtype, cast to it here
380 # error: Argument 1 to "as_unit" of "TimelikeOps" has
381 # incompatible type "str"; expected "Literal['s', 'ms', 'us', 'ns']"
382 # [arg-type]
383 result = result.as_unit(unit) # type: ignore[arg-type]
384
385 return result
386
387 @classmethod
388 def _generate_range(
389 cls,
390 start,
391 end,
392 periods: int | None,
393 freq,
394 tz=None,
395 normalize: bool = False,
396 ambiguous: TimeAmbiguous = "raise",
397 nonexistent: TimeNonexistent = "raise",
398 inclusive: IntervalClosedType = "both",
399 *,
400 unit: TimeUnit = "ns",
401 ) -> Self:
402 periods = dtl.validate_periods(periods)
403 if freq is None and any(x is None for x in [periods, start, end]):
404 raise ValueError("Must provide freq argument if no data is supplied")
405
406 if com.count_not_none(start, end, periods, freq) != 3:
407 raise ValueError(
408 "Of the four parameters: start, end, periods, "
409 "and freq, exactly three must be specified"
410 )
411 freq = to_offset(freq)
412
413 if start is not None:
414 start = Timestamp(start)
415
416 if end is not None:
417 end = Timestamp(end)
418
419 if start is NaT or end is NaT:
420 raise ValueError("Neither `start` nor `end` can be NaT")
421
422 if unit is not None:
423 if unit not in ["s", "ms", "us", "ns"]:
424 raise ValueError("'unit' must be one of 's', 'ms', 'us', 'ns'")
425 else:
426 unit = "ns"
427
428 if start is not None:
429 start = start.as_unit(unit, round_ok=False)
430 if end is not None:
431 end = end.as_unit(unit, round_ok=False)
432
433 left_inclusive, right_inclusive = validate_inclusive(inclusive)
434 start, end = _maybe_normalize_endpoints(start, end, normalize)
435 tz = _infer_tz_from_endpoints(start, end, tz)
436
437 if tz is not None:
438 # Localize the start and end arguments
439 start = _maybe_localize_point(start, freq, tz, ambiguous, nonexistent)
440 end = _maybe_localize_point(end, freq, tz, ambiguous, nonexistent)
441
442 if freq is not None:
443 # Offset handling:
444 # Ticks (fixed-duration like hours/minutes): keep tz; do absolute-time math.
445 # Other calendar offsets: drop tz; do naive wall time; localize once later
446 # so `ambiguous`/`nonexistent` are applied correctly.
447 if not isinstance(freq, Tick):
448 if start is not None and start.tz is not None:
449 start = start.tz_localize(None)
450 if end is not None and end.tz is not None:
451 end = end.tz_localize(None)
452
453 if isinstance(freq, (Tick, Day)):
454 i8values = generate_regular_range(start, end, periods, freq, unit=unit)
455 elif freq.n >= 1 and freq._supports_daily_offset_mask:
456 # GH#16463: fast "daily range + filter" path for offsets
457 # that support vectorized on-offset checks.
458 try:
459 i8values = generate_daily_offset_range(
460 start, end, periods, freq, unit=unit
461 )
462 except (OutOfBoundsDatetime, OutOfBoundsTimedelta, OverflowError):
463 # Near Timestamp bounds with periods; the iterative
464 # path handles this by stopping before overflow.
465 xdr = _generate_range(
466 start=start,
467 end=end,
468 periods=periods,
469 offset=freq,
470 unit=unit,
471 )
472 i8values = np.array([x._value for x in xdr], dtype=np.int64)
473 else:
474 xdr = _generate_range(
475 start=start,
476 end=end,
477 periods=periods,
478 offset=freq,
479 unit=unit,
480 )
481 i8values = np.array([x._value for x in xdr], dtype=np.int64)
482
483 endpoint_tz = start.tz if start is not None else end.tz
484
485 if tz is not None and endpoint_tz is None:
486 if not timezones.is_utc(tz):
487 # short-circuit tz_localize_to_utc which would make
488 # an unnecessary copy with UTC but be a no-op.
489 creso = abbrev_to_npy_unit(unit)
490 i8values = tzconversion.tz_localize_to_utc(
491 i8values,
492 tz,
493 ambiguous=ambiguous,
494 nonexistent=nonexistent,
495 creso=creso,
496 )
497
498 # i8values is localized datetime64 array -> have to convert
499 # start/end as well to compare
500 if start is not None:
501 start = start.tz_localize(tz, ambiguous, nonexistent)
502 if end is not None:
503 end = end.tz_localize(tz, ambiguous, nonexistent)
504 else:
505 # Create a linearly spaced date_range in local time
506 # Nanosecond-granularity timestamps aren't always correctly
507 # representable with doubles, so we limit the range that we
508 # pass to np.linspace as much as possible
509 periods = cast("int", periods)
510 i8values = (
511 np.linspace(0, end._value - start._value, periods, dtype="int64")
512 + start._value
513 )
514 if i8values.dtype != "i8":
515 # 2022-01-09 I (brock) am not sure if it is possible for this
516 # to overflow and cast to e.g. f8, but if it does we need to cast
517 i8values = i8values.astype("i8")
518
519 if start == end:
520 if not left_inclusive or not right_inclusive:
521 i8values = i8values[1:-1]
522 else:
523 start_i8 = (
524 Timestamp(start)._value
525 if start is not None
526 else i8values[0]
527 if len(i8values)
528 else 0
529 )
530 end_i8 = (
531 Timestamp(end)._value
532 if end is not None
533 else i8values[-1]
534 if len(i8values)
535 else 0
536 )
537 if not left_inclusive or not right_inclusive:
538 if not left_inclusive and len(i8values) and i8values[0] == start_i8:
539 i8values = i8values[1:]
540 if not right_inclusive and len(i8values) and i8values[-1] == end_i8:
541 i8values = i8values[:-1]
542
543 dt64_values = i8values.view(f"datetime64[{unit}]")
544 dtype = tz_to_dtype(tz, unit=unit)
545 return cls._simple_new(dt64_values, dtype=dtype)
546
547 # -----------------------------------------------------------------
548 # DatetimeLike Interface
549
550 def _unbox_scalar(self, value) -> np.datetime64:
551 if not isinstance(value, self._scalar_type) and value is not NaT:
552 raise ValueError("'value' should be a Timestamp.")
553 self._check_compatible_with(value)
554 if value is NaT:
555 return np.datetime64(value._value, self.unit)
556 else:
557 return value.as_unit(self.unit, round_ok=False).asm8
558
559 def _scalar_from_string(self, value) -> Timestamp | NaTType:
560 return Timestamp(value, tz=self.tz)
561
562 def _check_compatible_with(self, other) -> None:
563 if other is NaT:
564 return
565 self._assert_tzawareness_compat(other)
566
567 # -----------------------------------------------------------------
568 # Descriptive Properties
569
570 def _box_func(self, x: np.datetime64) -> Timestamp | NaTType:
571 # GH#42228
572 value = x.view("i8")
573 ts = Timestamp._from_value_and_reso(value, reso=self._creso, tz=self.tz)
574 return ts
575
576 @property
577 # error: Return type "Union[dtype, DatetimeTZDtype]" of "dtype"
578 # incompatible with return type "ExtensionDtype" in supertype
579 # "ExtensionArray"
580 def dtype(self) -> np.dtype[np.datetime64] | DatetimeTZDtype: # type: ignore[override]
581 """
582 The dtype for the DatetimeArray.
583
584 .. warning::
585
586 A future version of pandas will change dtype to never be a
587 ``numpy.dtype``. Instead, :attr:`DatetimeArray.dtype` will
588 always be an instance of an ``ExtensionDtype`` subclass.
589
590 Returns
591 -------
592 numpy.dtype or DatetimeTZDtype
593 If the values are tz-naive, then ``np.dtype('datetime64[ns]')``
594 is returned.
595
596 If the values are tz-aware, then the ``DatetimeTZDtype``
597 is returned.
598 """
599 return self._dtype
600
601 @property
602 def tz(self) -> tzinfo | None:
603 """
604 Return the timezone.
605
606 This property returns the timezone information associated with the
607 datetime data. If the data is timezone-naive (i.e. has no timezone
608 information), it returns None.
609
610 Returns
611 -------
612 zoneinfo.ZoneInfo, datetime.tzinfo, pytz.tzinfo.BaseTZInfo, dateutil.tz.tz.tzfile, or None
613 Returns None when the array is tz-naive.
614
615 See Also
616 --------
617 DatetimeIndex.tz_localize : Localize tz-naive DatetimeIndex to a
618 given time zone, or remove timezone from a tz-aware DatetimeIndex.
619 DatetimeIndex.tz_convert : Convert tz-aware DatetimeIndex from
620 one time zone to another.
621
622 Examples
623 --------
624 For Series:
625
626 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
627 >>> s = pd.to_datetime(s)
628 >>> s
629 0 2020-01-01 10:00:00+00:00
630 1 2020-02-01 11:00:00+00:00
631 dtype: datetime64[us, UTC]
632 >>> s.dt.tz
633 datetime.timezone.utc
634
635 For DatetimeIndex:
636
637 >>> idx = pd.DatetimeIndex(
638 ... ["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]
639 ... )
640 >>> idx.tz
641 datetime.timezone.utc
642 """ # noqa: E501
643 # GH 18595
644 return getattr(self.dtype, "tz", None)
645
646 @tz.setter
647 def tz(self, value):
648 # GH 3746: Prevent localizing or converting the index by setting tz
649 raise AttributeError(
650 "Cannot directly set timezone. Use tz_localize() "
651 "or tz_convert() as appropriate"
652 )
653
654 @property
655 def tzinfo(self) -> tzinfo | None:
656 """
657 Alias for tz attribute
658 """
659 return self.tz
660
661 @property # NB: override with cache_readonly in immutable subclasses
662 def is_normalized(self) -> bool:
663 """
664 Returns True if all of the dates are at midnight ("no time")
665 """
666 return is_date_array_normalized(self.asi8, self.tz, reso=self._creso)
667
668 # ----------------------------------------------------------------
669 # Array-Like / EA-Interface Methods
670
671 def __array__(self, dtype=None, copy=None) -> np.ndarray:
672 if dtype is None and self.tz:
673 # The default for tz-aware is object, to preserve tz info
674 dtype = object
675
676 return super().__array__(dtype=dtype, copy=copy)
677
678 def _iter_convert_chunk(self, data: np.ndarray) -> np.ndarray:
679 return ints_to_pydatetime(
680 data.view("i8"), tz=self.tz, box="timestamp", reso=self._creso
681 )
682
683 def astype(self, dtype, copy: bool = True):
684 # We handle
685 # --> datetime
686 # --> period
687 # DatetimeLikeArrayMixin Super handles the rest.
688 dtype = pandas_dtype(dtype)
689
690 if dtype == self.dtype:
691 if copy:
692 return self.copy()
693 return self
694
695 elif isinstance(dtype, ExtensionDtype):
696 if not isinstance(dtype, DatetimeTZDtype):
697 # e.g. Sparse[datetime64[ns]]
698 return super().astype(dtype, copy=copy)
699 elif self.tz is None:
700 # pre-2.0 this did self.tz_localize(dtype.tz), which did not match
701 # the Series behavior which did
702 # values.tz_localize("UTC").tz_convert(dtype.tz)
703 raise TypeError(
704 "Cannot use .astype to convert from timezone-naive dtype to "
705 "timezone-aware dtype. Use obj.tz_localize instead or "
706 "series.dt.tz_localize instead"
707 )
708 else:
709 # tzaware unit conversion e.g. datetime64[s, UTC]
710 np_dtype = np.dtype(dtype.str)
711 res_values = astype_overflowsafe(self._ndarray, np_dtype, copy=copy)
712 return type(self)._simple_new(res_values, dtype=dtype)
713
714 elif (
715 self.tz is None
716 and lib.is_np_dtype(dtype, "M")
717 and not is_unitless(dtype)
718 and is_supported_dtype(dtype)
719 ):
720 # unit conversion e.g. datetime64[s]
721 res_values = astype_overflowsafe(self._ndarray, dtype, copy=True)
722 return type(self)._simple_new(res_values, dtype=res_values.dtype)
723 # TODO: preserve freq?
724
725 elif self.tz is not None and lib.is_np_dtype(dtype, "M"):
726 # pre-2.0 behavior for DTA/DTI was
727 # values.tz_convert("UTC").tz_localize(None), which did not match
728 # the Series behavior
729 raise TypeError(
730 "Cannot use .astype to convert from timezone-aware dtype to "
731 "timezone-naive dtype. Use obj.tz_localize(None) or "
732 "obj.tz_convert('UTC').tz_localize(None) instead."
733 )
734
735 elif (
736 self.tz is None
737 and lib.is_np_dtype(dtype, "M")
738 and dtype != self.dtype
739 and is_unitless(dtype)
740 ):
741 raise TypeError(
742 "Casting to unit-less dtype 'datetime64' is not supported. "
743 "Pass e.g. 'datetime64[ns]' instead."
744 )
745
746 elif isinstance(dtype, PeriodDtype):
747 return self.to_period(freq=dtype.freq)
748 return dtl.DatetimeLikeArrayMixin.astype(self, dtype, copy)
749
750 # -----------------------------------------------------------------
751 # Rendering Methods
752
753 def _format_native_types(
754 self, *, na_rep: str | float = "NaT", date_format=None, **kwargs
755 ) -> npt.NDArray[np.object_]:
756 if date_format is None and self._is_dates_only:
757 # Only dates and no timezone: provide a default format
758 date_format = "%Y-%m-%d"
759
760 return tslib.format_array_from_datetime(
761 self.asi8, tz=self.tz, format=date_format, na_rep=na_rep, reso=self._creso
762 )
763
764 # -----------------------------------------------------------------
765 # Comparison Methods
766
767 def _assert_tzawareness_compat(self, other) -> None:
768 # adapted from _Timestamp._assert_tzawareness_compat
769 other_tz = getattr(other, "tzinfo", None)
770 other_dtype = getattr(other, "dtype", None)
771
772 if isinstance(other_dtype, DatetimeTZDtype):
773 # Get tzinfo from Series dtype
774 other_tz = other.dtype.tz
775 if other is NaT:
776 # pd.NaT quacks both aware and naive
777 pass
778 elif self.tz is None:
779 if other_tz is not None:
780 raise TypeError(
781 "Cannot compare tz-naive and tz-aware datetime-like objects."
782 )
783 elif other_tz is None:
784 raise TypeError(
785 "Cannot compare tz-naive and tz-aware datetime-like objects"
786 )
787
788 # -----------------------------------------------------------------
789 # Arithmetic Methods
790
791 def _add_offset(self, offset: BaseOffset) -> Self:
792 assert not isinstance(offset, Tick)
793
794 # For pure-timedelta DateOffset with tz-aware data, add to UTC values
795 # directly to avoid nonexistent/ambiguous time errors from
796 # re-localizing wall-time results near DST (GH#28610).
797 if (
798 self.tz is not None
799 and isinstance(offset, RelativeDeltaOffset)
800 and not offset._use_relativedelta
801 ):
802 # GH#64806 _add_timedeltalike_scalar casts to the finer of the two
803 # resolutions, so the result keeps offset._pd_timedelta's unit when
804 # that is finer than self's.
805 result = self._add_timedeltalike_scalar(offset._pd_timedelta)
806 if offset.normalize:
807 result = result.normalize()
808 return result
809
810 if self.tz is not None:
811 values = self.tz_localize(None)
812 else:
813 values = self
814
815 try:
816 res_values = offset._add_datetime_ndarray(values._ndarray)
817 if res_values.dtype.kind == "i":
818 # values is tz-naive here, so its dtype is the ndarray's
819 res_values = res_values.view(values._ndarray.dtype)
820 except NotImplementedError:
821 if config["mode"]["performance_warnings"]:
822 warnings.warn(
823 "Non-vectorized DateOffset being applied to Series or "
824 "DatetimeIndex.",
825 PerformanceWarning,
826 stacklevel=find_stack_level(),
827 )
828 res_values = self.astype("O") + offset
829 # GH#56586 use the finer resolution between self and offset.offset
830 # to avoid truncating sub-unit precision
831 res_unit = self.unit
832 if hasattr(offset, "offset") and offset.offset:
833 offset_unit = Timedelta(offset.offset).unit
834 if abbrev_to_npy_unit(offset_unit) > abbrev_to_npy_unit(res_unit):
835 res_unit = offset_unit
836 dtype = tz_to_dtype(self.tz, unit=res_unit)
837 result = type(self)._from_sequence(res_values, dtype=dtype)
838
839 else:
840 result = type(self)._simple_new(res_values, dtype=res_values.dtype)
841 if offset.normalize:
842 result = result.normalize()
843
844 if self.tz is not None:
845 result = result.tz_localize(self.tz)
846
847 return result
848
849 # -----------------------------------------------------------------
850 # Timezone Conversion and Localization Methods
851
852 def _local_timestamps(self) -> npt.NDArray[np.int64]:
853 """
854 Convert to an i8 (unix-like nanosecond timestamp) representation
855 while keeping the local timezone and not using UTC.
856 This is used to calculate time-of-day information as if the timestamps
857 were timezone-naive.
858 """
859 if self.tz is None or timezones.is_utc(self.tz):
860 # Avoid the copy that would be made in tzconversion
861 return self.asi8
862 return tz_convert_from_utc(self.asi8, self.tz, reso=self._creso)
863
864 def tz_convert(self, tz) -> Self:
865 """
866 Convert tz-aware Datetime Array/Index from one time zone to another.
867
868 This method converts datetime values from their current timezone to a
869 different timezone. The underlying UTC time remains the same, only the
870 local time representation changes to reflect the target timezone.
871
872 Parameters
873 ----------
874 tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None
875 Time zone for time. Corresponding timestamps would be converted
876 to this time zone of the Datetime Array/Index. A `tz` of None will
877 convert to UTC and remove the timezone information.
878
879 Returns
880 -------
881 Array or Index
882 Datetime Array/Index with target `tz`.
883
884 Raises
885 ------
886 TypeError
887 If Datetime Array/Index is tz-naive.
888
889 See Also
890 --------
891 DatetimeIndex.tz : A timezone that has a variable offset from UTC.
892 DatetimeIndex.tz_localize : Localize tz-naive DatetimeIndex to a
893 given time zone, or remove timezone from a tz-aware DatetimeIndex.
894 Series.dt.tz : A timezone that has a variable offset from UTC.
895 Series.dt.tz_localize : Localize tz-naive Series datetimes to a given
896 time zone, or remove timezone from tz-aware Series datetimes.
897
898 Examples
899 --------
900 With the `tz` parameter, we can change the DatetimeIndex
901 to other time zones:
902
903 >>> dti = pd.date_range(
904 ... start="2014-08-01 09:00", freq="h", periods=3, tz="Europe/Berlin"
905 ... )
906
907 >>> dti
908 DatetimeIndex(['2014-08-01 09:00:00+02:00',
909 '2014-08-01 10:00:00+02:00',
910 '2014-08-01 11:00:00+02:00'],
911 dtype='datetime64[us, Europe/Berlin]', freq='h')
912
913 >>> dti.tz_convert("US/Central")
914 DatetimeIndex(['2014-08-01 02:00:00-05:00',
915 '2014-08-01 03:00:00-05:00',
916 '2014-08-01 04:00:00-05:00'],
917 dtype='datetime64[us, US/Central]', freq='h')
918
919 With the ``tz=None``, we can remove the timezone (after converting
920 to UTC if necessary):
921
922 >>> dti = pd.date_range(
923 ... start="2014-08-01 09:00", freq="h", periods=3, tz="Europe/Berlin"
924 ... )
925
926 >>> dti
927 DatetimeIndex(['2014-08-01 09:00:00+02:00',
928 '2014-08-01 10:00:00+02:00',
929 '2014-08-01 11:00:00+02:00'],
930 dtype='datetime64[us, Europe/Berlin]', freq='h')
931
932 >>> dti.tz_convert(None)
933 DatetimeIndex(['2014-08-01 07:00:00',
934 '2014-08-01 08:00:00',
935 '2014-08-01 09:00:00'],
936 dtype='datetime64[us]', freq='h')
937 """ # noqa: E501
938 tz = timezones.maybe_get_tz(tz)
939
940 if self.tz is None:
941 # tz naive, use tz_localize
942 raise TypeError(
943 "Cannot convert tz-naive timestamps, use tz_localize to localize"
944 )
945
946 # No conversion since timestamps are all UTC to begin with
947 dtype = tz_to_dtype(tz, unit=self.unit)
948 return self._simple_new(self._ndarray, dtype=dtype)
949
950 @dtl.ravel_compat
951 def tz_localize(
952 self,
953 tz,
954 ambiguous: TimeAmbiguous = "raise",
955 nonexistent: TimeNonexistent = "raise",
956 ) -> Self:
957 """
958 Localize tz-naive Datetime Array/Index to tz-aware Datetime Array/Index.
959
960 This method takes a time zone (tz) naive Datetime Array/Index object
961 and makes this time zone aware. It does not move the time to another
962 time zone.
963
964 This method can also be used to do the inverse -- to create a time
965 zone unaware object from an aware object. To that end, pass `tz=None`.
966
967 Parameters
968 ----------
969 tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None
970 Time zone to attach to the tz-naive timestamps; the wall time is
971 preserved. Passing ``None`` detaches the time zone from a tz-aware
972 Array/Index, returning a tz-naive result with the same wall time.
973 ambiguous : 'infer', 'NaT', bool array, default 'raise'
974 When clocks moved backward due to DST, ambiguous times may arise.
975 For example in Central European Time (UTC+01), when going from
976 03:00 DST to 02:00 non-DST, 02:30:00 local time occurs both at
977 00:30:00 UTC and at 01:30:00 UTC. In such a situation, the
978 `ambiguous` parameter dictates how ambiguous times should be
979 handled.
980
981 - 'infer' will attempt to infer fall dst-transition hours based on
982 order. Requires that the timestamps are monotonically increasing.
983 - bool-ndarray where True signifies a DST time, False signifies a
984 non-DST time (note that this flag is only applicable for
985 ambiguous times)
986 - 'NaT' will return NaT where there are ambiguous times
987 - 'raise' will raise a ValueError if there are ambiguous
988 times.
989
990 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \
991default 'raise'
992 A nonexistent time does not exist in a particular timezone
993 where clocks moved forward due to DST.
994
995 - 'shift_forward' will shift the nonexistent time forward to the
996 closest existing time
997 - 'shift_backward' will shift the nonexistent time backward to the
998 closest existing time
999 - 'NaT' will return NaT where there are nonexistent times
1000 - timedelta objects will shift nonexistent times by the timedelta
1001 - 'raise' will raise a ValueError if there are
1002 nonexistent times.
1003
1004 Returns
1005 -------
1006 Same type as self
1007 Array/Index converted to the specified time zone.
1008
1009 Raises
1010 ------
1011 TypeError
1012 If the Datetime Array/Index is tz-aware and tz is not None.
1013
1014 See Also
1015 --------
1016 DatetimeIndex.tz_convert : Convert tz-aware DatetimeIndex from
1017 one time zone to another.
1018
1019 Examples
1020 --------
1021 >>> tz_naive = pd.date_range('2018-03-01 09:00', periods=3)
1022 >>> tz_naive
1023 DatetimeIndex(['2018-03-01 09:00:00', '2018-03-02 09:00:00',
1024 '2018-03-03 09:00:00'],
1025 dtype='datetime64[us]', freq='D')
1026
1027 Localize DatetimeIndex in US/Eastern time zone:
1028
1029 >>> tz_aware = tz_naive.tz_localize(tz='US/Eastern')
1030 >>> tz_aware
1031 DatetimeIndex(['2018-03-01 09:00:00-05:00', '2018-03-02 09:00:00-05:00',
1032 '2018-03-03 09:00:00-05:00'],
1033 dtype='datetime64[us, US/Eastern]', freq='D')
1034
1035 With ``tz=None`` we can remove the time zone information while
1036 preserving the wall time (no conversion to UTC):
1037
1038 >>> tz_aware.tz_localize(None)
1039 DatetimeIndex(['2018-03-01 09:00:00', '2018-03-02 09:00:00',
1040 '2018-03-03 09:00:00'],
1041 dtype='datetime64[us]', freq='D')
1042
1043 Be careful with DST changes. When there is sequential data, pandas can
1044 infer the DST time:
1045
1046 >>> s = pd.to_datetime(pd.Series(['2018-10-28 01:30:00',
1047 ... '2018-10-28 02:00:00',
1048 ... '2018-10-28 02:30:00',
1049 ... '2018-10-28 02:00:00',
1050 ... '2018-10-28 02:30:00',
1051 ... '2018-10-28 03:00:00',
1052 ... '2018-10-28 03:30:00']))
1053 >>> s.dt.tz_localize('CET', ambiguous='infer')
1054 0 2018-10-28 01:30:00+02:00
1055 1 2018-10-28 02:00:00+02:00
1056 2 2018-10-28 02:30:00+02:00
1057 3 2018-10-28 02:00:00+01:00
1058 4 2018-10-28 02:30:00+01:00
1059 5 2018-10-28 03:00:00+01:00
1060 6 2018-10-28 03:30:00+01:00
1061 dtype: datetime64[us, CET]
1062
1063 In some cases, inferring the DST is impossible. In such cases, you can
1064 pass an ndarray to the ambiguous parameter to set the DST explicitly
1065
1066 >>> s = pd.to_datetime(pd.Series(['2018-10-28 01:20:00',
1067 ... '2018-10-28 02:36:00',
1068 ... '2018-10-28 03:46:00']))
1069 >>> s.dt.tz_localize('CET', ambiguous=np.array([True, True, False]))
1070 0 2018-10-28 01:20:00+02:00
1071 1 2018-10-28 02:36:00+02:00
1072 2 2018-10-28 03:46:00+01:00
1073 dtype: datetime64[us, CET]
1074
1075 If the DST transition causes nonexistent times, you can shift these
1076 dates forward or backwards with a timedelta object or `'shift_forward'`
1077 or `'shift_backward'`.
1078
1079 >>> s = pd.to_datetime(pd.Series(['2015-03-29 02:30:00',
1080 ... '2015-03-29 03:30:00'], dtype="M8[ns]"))
1081 >>> s.dt.tz_localize('Europe/Warsaw', nonexistent='shift_forward')
1082 0 2015-03-29 03:00:00+02:00
1083 1 2015-03-29 03:30:00+02:00
1084 dtype: datetime64[ns, Europe/Warsaw]
1085
1086 >>> s.dt.tz_localize('Europe/Warsaw', nonexistent='shift_backward')
1087 0 2015-03-29 01:59:59.999999999+01:00
1088 1 2015-03-29 03:30:00.000000000+02:00
1089 dtype: datetime64[ns, Europe/Warsaw]
1090
1091 >>> s.dt.tz_localize('Europe/Warsaw', nonexistent=pd.Timedelta('1h'))
1092 0 2015-03-29 03:30:00+02:00
1093 1 2015-03-29 03:30:00+02:00
1094 dtype: datetime64[ns, Europe/Warsaw]
1095 """ # noqa: E501
1096 nonexistent_options = ("raise", "NaT", "shift_forward", "shift_backward")
1097 if nonexistent not in nonexistent_options and not isinstance(
1098 nonexistent, timedelta
1099 ):
1100 raise ValueError(
1101 "The nonexistent argument must be one of 'raise', "
1102 "'NaT', 'shift_forward', 'shift_backward' or "
1103 "a timedelta object"
1104 )
1105
1106 if self.tz is not None:
1107 if tz is None:
1108 new_dates = tz_convert_from_utc(self.asi8, self.tz, reso=self._creso)
1109 else:
1110 raise TypeError("Already tz-aware, use tz_convert to convert.")
1111 else:
1112 tz = timezones.maybe_get_tz(tz)
1113 # Convert to UTC
1114
1115 new_dates = tzconversion.tz_localize_to_utc(
1116 self.asi8,
1117 tz,
1118 ambiguous=ambiguous,
1119 nonexistent=nonexistent,
1120 creso=self._creso,
1121 )
1122 new_dates_dt64 = new_dates.view(f"M8[{self.unit}]")
1123 dtype = tz_to_dtype(tz, unit=self.unit)
1124
1125 return self._simple_new(new_dates_dt64, dtype=dtype)
1126
1127 # ----------------------------------------------------------------
1128 # Conversion Methods - Vectorized analogues of Timestamp methods
1129
1130 def to_pydatetime(self) -> npt.NDArray[np.object_]:
1131 """
1132 Return an ndarray of ``datetime.datetime`` objects.
1133
1134 This method converts each element in the DatetimeArray to a native
1135 Python ``datetime.datetime`` object, including timezone information
1136 if present.
1137
1138 Returns
1139 -------
1140 numpy.ndarray
1141 An ndarray of ``datetime.datetime`` objects.
1142
1143 See Also
1144 --------
1145 DatetimeIndex.to_julian_date : Converts Datetime Array to float64 ndarray
1146 of Julian Dates.
1147
1148 Examples
1149 --------
1150 >>> idx = pd.date_range("2018-02-27", periods=3)
1151 >>> idx.to_pydatetime()
1152 array([datetime.datetime(2018, 2, 27, 0, 0),
1153 datetime.datetime(2018, 2, 28, 0, 0),
1154 datetime.datetime(2018, 3, 1, 0, 0)], dtype=object)
1155 """
1156 return ints_to_pydatetime(self.asi8, tz=self.tz, reso=self._creso)
1157
1158 def normalize(self) -> Self:
1159 """
1160 Convert times to midnight.
1161
1162 The time component of the date-time is converted to midnight i.e.
1163 00:00:00. This is useful in cases, when the time does not matter.
1164 Length is unaltered. The timezones are unaffected.
1165
1166 This method is available on Series with datetime values under
1167 the ``.dt`` accessor, and directly on Datetime Array/Index.
1168
1169 Returns
1170 -------
1171 DatetimeArray, DatetimeIndex or Series
1172 The same type as the original data. Series will have the same
1173 name and index. DatetimeIndex will have the same name.
1174
1175 See Also
1176 --------
1177 floor : Floor the datetimes to the specified freq.
1178 ceil : Ceil the datetimes to the specified freq.
1179 round : Round the datetimes to the specified freq.
1180
1181 Examples
1182 --------
1183 >>> idx = pd.date_range(
1184 ... start="2014-08-01 10:00", freq="h", periods=3, tz="Asia/Calcutta"
1185 ... )
1186 >>> idx
1187 DatetimeIndex(['2014-08-01 10:00:00+05:30',
1188 '2014-08-01 11:00:00+05:30',
1189 '2014-08-01 12:00:00+05:30'],
1190 dtype='datetime64[us, Asia/Calcutta]', freq='h')
1191 >>> idx.normalize()
1192 DatetimeIndex(['2014-08-01 00:00:00+05:30',
1193 '2014-08-01 00:00:00+05:30',
1194 '2014-08-01 00:00:00+05:30'],
1195 dtype='datetime64[us, Asia/Calcutta]', freq=None)
1196 """
1197 dta = self._normalize_naive()
1198 if self.tz is not None:
1199 dta = dta.tz_localize(self.tz)
1200 return dta
1201
1202 def _normalize_naive(self) -> Self:
1203 """Normalize times to midnight, ignoring the timezone."""
1204 new_values = normalize_i8_timestamps(self.asi8, self.tz, reso=self._creso)
1205 dt64_values = new_values.view(self._ndarray.dtype)
1206 return type(self)._simple_new(dt64_values, dtype=dt64_values.dtype)
1207
1208 def to_period(self, freq=None) -> PeriodArray:
1209 """
1210 Cast to PeriodArray/PeriodIndex at a particular frequency.
1211
1212 Converts DatetimeArray/Index to PeriodArray/PeriodIndex.
1213
1214 Parameters
1215 ----------
1216 freq : str, optional
1217 One of pandas' :ref:`period aliases <timeseries.period_aliases>`.
1218 Will be inferred by default.
1219
1220 Returns
1221 -------
1222 PeriodArray/PeriodIndex
1223 Immutable ndarray holding ordinal values at a particular frequency.
1224
1225 Raises
1226 ------
1227 ValueError
1228 When converting a DatetimeArray/Index with non-regular values,
1229 so that a frequency cannot be inferred.
1230
1231 See Also
1232 --------
1233 PeriodIndex: Immutable ndarray holding ordinal values.
1234 DatetimeIndex.to_pydatetime: Return DatetimeIndex as object.
1235
1236 Examples
1237 --------
1238 >>> df = pd.DataFrame(
1239 ... {"y": [1, 2, 3]},
1240 ... index=pd.to_datetime(
1241 ... [
1242 ... "2000-03-31 00:00:00",
1243 ... "2000-05-31 00:00:00",
1244 ... "2000-08-31 00:00:00",
1245 ... ]
1246 ... ),
1247 ... )
1248 >>> df.index.to_period("M")
1249 PeriodIndex(['2000-03', '2000-05', '2000-08'],
1250 dtype='period[M]')
1251
1252 Infer the daily frequency
1253
1254 >>> idx = pd.date_range("2017-01-01", periods=2)
1255 >>> idx.to_period()
1256 PeriodIndex(['2017-01-01', '2017-01-02'],
1257 dtype='period[D]')
1258 """
1259 from pandas.core.arrays import PeriodArray
1260
1261 if self.tz is not None:
1262 warnings.warn(
1263 "Converting to PeriodArray/Index representation "
1264 "will drop timezone information.",
1265 UserWarning,
1266 stacklevel=find_stack_level(),
1267 )
1268
1269 if freq is None:
1270 freq = self._inferred_freq_str
1271
1272 if freq is None:
1273 raise ValueError(
1274 "You must pass a freq argument as current index has none."
1275 )
1276
1277 res = get_period_alias(freq)
1278
1279 # https://github.com/pandas-dev/pandas/issues/33358
1280 if res is None:
1281 res = freq
1282
1283 freq = res
1284 return PeriodArray._from_datetime64(self._ndarray, freq, tz=self.tz)
1285
1286 # -----------------------------------------------------------------
1287 # Properties - Vectorized Timestamp Properties/Methods
1288
1289 def month_name(self, locale=None) -> npt.NDArray[np.object_]:
1290 """
1291 Return the month names with specified locale.
1292
1293 This method returns the full name of the month (e.g., "January", "February")
1294 for each datetime value in the Series/Index. The names can be localized
1295 to different languages using the locale parameter.
1296
1297 Parameters
1298 ----------
1299 locale : str, optional
1300 Locale determining the language in which to return the month name.
1301 Default is English locale (``'en_US.utf8'``). Use the command
1302 ``locale -a`` on your terminal on Unix systems to find your locale
1303 language code.
1304
1305 Returns
1306 -------
1307 Series or Index
1308 Series or Index of month names.
1309
1310 See Also
1311 --------
1312 DatetimeIndex.day_name : Return the day names with specified locale.
1313
1314 Examples
1315 --------
1316 >>> s = pd.Series(pd.date_range(start="2018-01", freq="ME", periods=3))
1317 >>> s
1318 0 2018-01-31
1319 1 2018-02-28
1320 2 2018-03-31
1321 dtype: datetime64[us]
1322 >>> s.dt.month_name()
1323 0 January
1324 1 February
1325 2 March
1326 dtype: str
1327
1328 >>> idx = pd.date_range(start="2018-01", freq="ME", periods=3)
1329 >>> idx
1330 DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31'],
1331 dtype='datetime64[us]', freq='ME')
1332 >>> idx.month_name()
1333 Index(['January', 'February', 'March'], dtype='str')
1334
1335 Using the ``locale`` parameter you can set a different locale language,
1336 for example: ``idx.month_name(locale='pt_BR.utf8')`` will return month
1337 names in Brazilian Portuguese language.
1338
1339 >>> idx = pd.date_range(start="2018-01", freq="ME", periods=3)
1340 >>> idx
1341 DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31'],
1342 dtype='datetime64[us]', freq='ME')
1343 >>> idx.month_name(locale="pt_BR.utf8") # doctest: +SKIP
1344 Index(['Janeiro', 'Fevereiro', 'Março'], dtype='str')
1345 """
1346 if locale is None and HAS_PYARROW and using_string_dtype():
1347 return self._month_name_arrow() # type: ignore[return-value]
1348
1349 values = self._local_timestamps()
1350
1351 result = fields.get_date_name_field(
1352 values, "month_name", locale=locale, reso=self._creso
1353 )
1354 result = self._maybe_mask_results(result, fill_value=None)
1355 if using_string_dtype():
1356 from pandas import (
1357 StringDtype,
1358 array as pd_array,
1359 )
1360
1361 return pd_array(result, dtype=StringDtype(na_value=np.nan)) # type: ignore[return-value]
1362 return result
1363
1364 def _month_name_arrow(self) -> ArrowStringArray:
1365 import pyarrow as pa
1366 import pyarrow.compute as pc
1367
1368 pa_months = pa.array(MONTHS_FULL, type=pa.large_string())
1369 pa_ts = self._to_local_pa_timestamp()
1370 result = pc.take(pa_months, pc.month(pa_ts))
1371 return self._wrap_pa_str_result(result)
1372
1373 def day_name(self, locale=None) -> npt.NDArray[np.object_]:
1374 """
1375 Return the day names with specified locale.
1376
1377 This method returns the full name of the day of the week (e.g., "Monday",
1378 "Tuesday") for each datetime value in the Series/Index. The names can be
1379 localized to different languages using the locale parameter.
1380
1381 Parameters
1382 ----------
1383 locale : str, optional
1384 Locale determining the language in which to return the day name.
1385 Default is English locale (``'en_US.utf8'``). Use the command
1386 ``locale -a`` on your terminal on Unix systems to find your locale
1387 language code.
1388
1389 Returns
1390 -------
1391 Series or Index
1392 Series or Index of day names.
1393
1394 See Also
1395 --------
1396 DatetimeIndex.month_name : Return the month names with specified locale.
1397
1398 Examples
1399 --------
1400 >>> s = pd.Series(pd.date_range(start="2018-01-01", freq="D", periods=3))
1401 >>> s
1402 0 2018-01-01
1403 1 2018-01-02
1404 2 2018-01-03
1405 dtype: datetime64[us]
1406 >>> s.dt.day_name()
1407 0 Monday
1408 1 Tuesday
1409 2 Wednesday
1410 dtype: str
1411
1412 >>> idx = pd.date_range(start="2018-01-01", freq="D", periods=3)
1413 >>> idx
1414 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03'],
1415 dtype='datetime64[us]', freq='D')
1416 >>> idx.day_name()
1417 Index(['Monday', 'Tuesday', 'Wednesday'], dtype='str')
1418
1419 Using the ``locale`` parameter you can set a different locale language,
1420 for example: ``idx.day_name(locale='pt_BR.utf8')`` will return day
1421 names in Brazilian Portuguese language.
1422
1423 >>> idx = pd.date_range(start="2018-01-01", freq="D", periods=3)
1424 >>> idx
1425 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03'],
1426 dtype='datetime64[us]', freq='D')
1427 >>> idx.day_name(locale="pt_BR.utf8") # doctest: +SKIP
1428 Index(['Segunda', 'Terça', 'Quarta'], dtype='str')
1429 """
1430 if locale is None and HAS_PYARROW and using_string_dtype():
1431 return self._day_name_arrow() # type: ignore[return-value]
1432
1433 values = self._local_timestamps()
1434
1435 result = fields.get_date_name_field(
1436 values, "day_name", locale=locale, reso=self._creso
1437 )
1438 result = self._maybe_mask_results(result, fill_value=None)
1439 if using_string_dtype():
1440 from pandas import (
1441 StringDtype,
1442 array as pd_array,
1443 )
1444
1445 return pd_array(result, dtype=StringDtype(na_value=np.nan)) # type: ignore[return-value]
1446 return result
1447
1448 def _day_name_arrow(self) -> ArrowStringArray:
1449 import pyarrow as pa
1450 import pyarrow.compute as pc
1451
1452 pa_days = pa.array(DAYS_FULL, type=pa.large_string())
1453 pa_ts = self._to_local_pa_timestamp()
1454 result = pc.take(pa_days, pc.day_of_week(pa_ts))
1455 return self._wrap_pa_str_result(result)
1456
1457 def _to_local_pa_timestamp(self) -> pa.Array:
1458 """Convert to a pyarrow TimestampArray with local timestamps."""
1459 import pyarrow as pa
1460
1461 values = self._local_timestamps()
1462 mask = self._isnan
1463 return pa.array(values, mask=mask, type=pa.timestamp(self.unit))
1464
1465 @staticmethod
1466 def _wrap_pa_str_result(pa_arr: pa.Array) -> ArrowStringArray:
1467 """Wrap a pyarrow StringArray in an ArrowStringArray with StringDtype."""
1468 from pandas.core.arrays.string_ import StringDtype
1469
1470 dtype = StringDtype(na_value=np.nan)
1471 cls = dtype.construct_array_type()
1472 return cls(pa_arr, dtype=dtype) # type: ignore[call-arg, return-value]
1473
1474 @property
1475 def time(self) -> npt.NDArray[np.object_]:
1476 """
1477 Returns numpy array of :class:`datetime.time` objects.
1478
1479 The time part of the Timestamps.
1480
1481 See Also
1482 --------
1483 DatetimeIndex.timetz : Returns numpy array of :class:`datetime.time`
1484 objects with timezones. The time part of the Timestamps.
1485 DatetimeIndex.date : Returns numpy array of python :class:`datetime.date`
1486 objects. Namely, the date part of Timestamps without time and timezone
1487 information.
1488
1489 Examples
1490 --------
1491 For Series:
1492
1493 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
1494 >>> s = pd.to_datetime(s)
1495 >>> s
1496 0 2020-01-01 10:00:00+00:00
1497 1 2020-02-01 11:00:00+00:00
1498 dtype: datetime64[us, UTC]
1499 >>> s.dt.time
1500 0 10:00:00
1501 1 11:00:00
1502 dtype: object
1503
1504 For DatetimeIndex:
1505
1506 >>> idx = pd.DatetimeIndex(
1507 ... ["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]
1508 ... )
1509 >>> idx.time
1510 array([datetime.time(10, 0), datetime.time(11, 0)], dtype=object)
1511 """
1512 # If the Timestamps have a timezone that is not UTC,
1513 # convert them into their i8 representation while
1514 # keeping their timezone and not using UTC
1515 timestamps = self._local_timestamps()
1516
1517 return ints_to_pydatetime(timestamps, box="time", reso=self._creso)
1518
1519 @property
1520 def timetz(self) -> npt.NDArray[np.object_]:
1521 """
1522 Returns numpy array of :class:`datetime.time` objects with timezones.
1523
1524 The time part of the Timestamps.
1525
1526 See Also
1527 --------
1528 DatetimeIndex.time : Returns numpy array of :class:`datetime.time` objects.
1529 The time part of the Timestamps.
1530 DatetimeIndex.tz : Return the timezone.
1531
1532 Examples
1533 --------
1534 For Series:
1535
1536 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
1537 >>> s = pd.to_datetime(s)
1538 >>> s
1539 0 2020-01-01 10:00:00+00:00
1540 1 2020-02-01 11:00:00+00:00
1541 dtype: datetime64[us, UTC]
1542 >>> s.dt.timetz
1543 0 10:00:00+00:00
1544 1 11:00:00+00:00
1545 dtype: object
1546
1547 For DatetimeIndex:
1548
1549 >>> idx = pd.DatetimeIndex(
1550 ... ["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]
1551 ... )
1552 >>> idx.timetz
1553 array([datetime.time(10, 0, tzinfo=datetime.timezone.utc),
1554 datetime.time(11, 0, tzinfo=datetime.timezone.utc)], dtype=object)
1555 """
1556 return ints_to_pydatetime(self.asi8, self.tz, box="time", reso=self._creso)
1557
1558 @property
1559 def date(self) -> npt.NDArray[np.object_]:
1560 """
1561 Returns numpy array of python :class:`datetime.date` objects.
1562
1563 Namely, the date part of Timestamps without time and
1564 timezone information.
1565
1566 See Also
1567 --------
1568 DatetimeIndex.time : Returns numpy array of :class:`datetime.time` objects.
1569 The time part of the Timestamps.
1570 DatetimeIndex.year : The year of the datetime.
1571 DatetimeIndex.month : The month as January=1, December=12.
1572 DatetimeIndex.day : The day of the datetime.
1573
1574 Examples
1575 --------
1576 For Series:
1577
1578 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
1579 >>> s = pd.to_datetime(s)
1580 >>> s
1581 0 2020-01-01 10:00:00+00:00
1582 1 2020-02-01 11:00:00+00:00
1583 dtype: datetime64[us, UTC]
1584 >>> s.dt.date
1585 0 2020-01-01
1586 1 2020-02-01
1587 dtype: object
1588
1589 For DatetimeIndex:
1590
1591 >>> idx = pd.DatetimeIndex(
1592 ... ["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"]
1593 ... )
1594 >>> idx.date
1595 array([datetime.date(2020, 1, 1), datetime.date(2020, 2, 1)], dtype=object)
1596 """
1597 # If the Timestamps have a timezone that is not UTC,
1598 # convert them into their i8 representation while
1599 # keeping their timezone and not using UTC
1600 timestamps = self._local_timestamps()
1601
1602 return ints_to_pydatetime(timestamps, box="date", reso=self._creso)
1603
1604 def isocalendar(self) -> DataFrame:
1605 """
1606 Calculate year, week, and day according to the ISO 8601 standard.
1607
1608 The ISO 8601 standard defines the first week of the year as the week
1609 containing the first Thursday. This method returns a DataFrame with
1610 columns for the ISO year, ISO week number, and ISO day of week.
1611
1612 Returns
1613 -------
1614 DataFrame
1615 With columns year, week and day.
1616
1617 See Also
1618 --------
1619 Timestamp.isocalendar : Function return a 3-tuple containing ISO year,
1620 week number, and weekday for the given Timestamp object.
1621 datetime.date.isocalendar : Return a named tuple object with
1622 three components: year, week and weekday.
1623
1624 Examples
1625 --------
1626 >>> idx = pd.date_range(start="2019-12-29", freq="D", periods=4)
1627 >>> idx.isocalendar()
1628 year week day
1629 2019-12-29 2019 52 7
1630 2019-12-30 2020 1 1
1631 2019-12-31 2020 1 2
1632 2020-01-01 2020 1 3
1633 >>> idx.isocalendar().week
1634 2019-12-29 52
1635 2019-12-30 1
1636 2019-12-31 1
1637 2020-01-01 1
1638 Freq: D, Name: week, dtype: UInt32
1639 """
1640 from pandas import DataFrame
1641
1642 values = self._local_timestamps()
1643 sarray = fields.build_isocalendar_sarray(values, reso=self._creso)
1644 iso_calendar_df = DataFrame(sarray, columns=["year", "week", "day"]).astype(
1645 {"year": "Int32", "week": "UInt32", "day": "UInt32"}
1646 )
1647 if self._hasna:
1648 iso_calendar_df.iloc[self._isnan] = None
1649 return iso_calendar_df
1650
1651 year = _field_accessor(
1652 "year",
1653 "Y",
1654 """
1655 The year of the datetime.
1656
1657 This attribute returns the year component of each datetime value
1658 in the Series/Index. The returned values are integers representing
1659 the calendar year.
1660
1661 See Also
1662 --------
1663 DatetimeIndex.month: The month as January=1, December=12.
1664 DatetimeIndex.day: The day of the datetime.
1665
1666 Examples
1667 --------
1668 >>> datetime_series = pd.Series(
1669 ... pd.date_range("2000-01-01", periods=3, freq="YE")
1670 ... )
1671 >>> datetime_series
1672 0 2000-12-31
1673 1 2001-12-31
1674 2 2002-12-31
1675 dtype: datetime64[us]
1676 >>> datetime_series.dt.year
1677 0 2000
1678 1 2001
1679 2 2002
1680 dtype: int32
1681 """,
1682 )
1683 month = _field_accessor(
1684 "month",
1685 "M",
1686 """
1687 The month as January=1, December=12.
1688
1689 This attribute returns the month component of each datetime value
1690 in the Series/Index. Months are numbered from 1 (January) through
1691 12 (December).
1692
1693 See Also
1694 --------
1695 DatetimeIndex.year: The year of the datetime.
1696 DatetimeIndex.day: The day of the datetime.
1697
1698 Examples
1699 --------
1700 >>> datetime_series = pd.Series(
1701 ... pd.date_range("2000-01-01", periods=3, freq="ME")
1702 ... )
1703 >>> datetime_series
1704 0 2000-01-31
1705 1 2000-02-29
1706 2 2000-03-31
1707 dtype: datetime64[us]
1708 >>> datetime_series.dt.month
1709 0 1
1710 1 2
1711 2 3
1712 dtype: int32
1713 """,
1714 )
1715 day = _field_accessor(
1716 "day",
1717 "D",
1718 """
1719 The day of the datetime.
1720
1721 This attribute returns the day of the month component of each
1722 datetime value in the Series/Index. Values range from 1 to 31,
1723 depending on the month.
1724
1725 See Also
1726 --------
1727 DatetimeIndex.year: The year of the datetime.
1728 DatetimeIndex.month: The month as January=1, December=12.
1729 DatetimeIndex.hour: The hours of the datetime.
1730
1731 Examples
1732 --------
1733 >>> datetime_series = pd.Series(
1734 ... pd.date_range("2000-01-01", periods=3, freq="D")
1735 ... )
1736 >>> datetime_series
1737 0 2000-01-01
1738 1 2000-01-02
1739 2 2000-01-03
1740 dtype: datetime64[us]
1741 >>> datetime_series.dt.day
1742 0 1
1743 1 2
1744 2 3
1745 dtype: int32
1746 """,
1747 )
1748 hour = _field_accessor(
1749 "hour",
1750 "h",
1751 """
1752 The hours of the datetime.
1753
1754 This attribute returns the hour component of each datetime value
1755 in the Series/Index. Values range from 0 to 23, representing the
1756 hour of the day in 24-hour format.
1757
1758 See Also
1759 --------
1760 DatetimeIndex.day: The day of the datetime.
1761 DatetimeIndex.minute: The minutes of the datetime.
1762 DatetimeIndex.second: The seconds of the datetime.
1763
1764 Examples
1765 --------
1766 >>> datetime_series = pd.Series(
1767 ... pd.date_range("2000-01-01", periods=3, freq="h")
1768 ... )
1769 >>> datetime_series
1770 0 2000-01-01 00:00:00
1771 1 2000-01-01 01:00:00
1772 2 2000-01-01 02:00:00
1773 dtype: datetime64[us]
1774 >>> datetime_series.dt.hour
1775 0 0
1776 1 1
1777 2 2
1778 dtype: int32
1779 """,
1780 )
1781 minute = _field_accessor(
1782 "minute",
1783 "m",
1784 """
1785 The minutes of the datetime.
1786
1787 This attribute returns the minute component of each datetime value
1788 in the Series/Index. Values range from 0 to 59.
1789
1790 See Also
1791 --------
1792 DatetimeIndex.hour: The hours of the datetime.
1793 DatetimeIndex.second: The seconds of the datetime.
1794
1795 Examples
1796 --------
1797 >>> datetime_series = pd.Series(
1798 ... pd.date_range("2000-01-01", periods=3, freq="min")
1799 ... )
1800 >>> datetime_series
1801 0 2000-01-01 00:00:00
1802 1 2000-01-01 00:01:00
1803 2 2000-01-01 00:02:00
1804 dtype: datetime64[us]
1805 >>> datetime_series.dt.minute
1806 0 0
1807 1 1
1808 2 2
1809 dtype: int32
1810 """,
1811 )
1812 second = _field_accessor(
1813 "second",
1814 "s",
1815 """
1816 The seconds of the datetime.
1817
1818 This attribute returns the second component of each datetime value
1819 in the Series/Index. Values range from 0 to 59.
1820
1821 See Also
1822 --------
1823 DatetimeIndex.minute: The minutes of the datetime.
1824 DatetimeIndex.microsecond: The microseconds of the datetime.
1825 DatetimeIndex.nanosecond: The nanoseconds of the datetime.
1826
1827 Examples
1828 --------
1829 >>> datetime_series = pd.Series(
1830 ... pd.date_range("2000-01-01", periods=3, freq="s")
1831 ... )
1832 >>> datetime_series
1833 0 2000-01-01 00:00:00
1834 1 2000-01-01 00:00:01
1835 2 2000-01-01 00:00:02
1836 dtype: datetime64[us]
1837 >>> datetime_series.dt.second
1838 0 0
1839 1 1
1840 2 2
1841 dtype: int32
1842 """,
1843 )
1844 microsecond = _field_accessor(
1845 "microsecond",
1846 "us",
1847 """
1848 The microseconds of the datetime.
1849
1850 This attribute returns the microsecond component of each datetime
1851 value in the Series/Index. Values range from 0 to 999999
1852 (one microsecond is one millionth of a second).
1853
1854 See Also
1855 --------
1856 DatetimeIndex.second: The seconds of the datetime.
1857 DatetimeIndex.nanosecond: The nanoseconds of the datetime.
1858
1859 Examples
1860 --------
1861 >>> datetime_series = pd.Series(
1862 ... pd.date_range("2000-01-01", periods=3, freq="us")
1863 ... )
1864 >>> datetime_series
1865 0 2000-01-01 00:00:00.000000
1866 1 2000-01-01 00:00:00.000001
1867 2 2000-01-01 00:00:00.000002
1868 dtype: datetime64[us]
1869 >>> datetime_series.dt.microsecond
1870 0 0
1871 1 1
1872 2 2
1873 dtype: int32
1874 """,
1875 )
1876 nanosecond = _field_accessor(
1877 "nanosecond",
1878 "ns",
1879 """
1880 The nanoseconds of the datetime.
1881
1882 This attribute returns the nanosecond component of each datetime
1883 value in the Series/Index. Values range from 0 to 999
1884 (one nanosecond is one billionth of a second).
1885
1886 See Also
1887 --------
1888 DatetimeIndex.second: The seconds of the datetime.
1889 DatetimeIndex.microsecond: The microseconds of the datetime.
1890
1891 Examples
1892 --------
1893 >>> datetime_series = pd.Series(
1894 ... pd.date_range("2000-01-01", periods=3, freq="ns")
1895 ... )
1896 >>> datetime_series
1897 0 2000-01-01 00:00:00.000000000
1898 1 2000-01-01 00:00:00.000000001
1899 2 2000-01-01 00:00:00.000000002
1900 dtype: datetime64[ns]
1901 >>> datetime_series.dt.nanosecond
1902 0 0
1903 1 1
1904 2 2
1905 dtype: int32
1906 """,
1907 )
1908 _dayofweek_doc = """
1909 The day of the week with Monday=0, Sunday=6.
1910
1911 Return the day of the week. It is assumed the week starts on
1912 Monday, which is denoted by 0 and ends on Sunday which is denoted
1913 by 6. This method is available on both Series with datetime
1914 values (using the `dt` accessor) or DatetimeIndex.
1915
1916 Returns
1917 -------
1918 Series or Index
1919 Containing integers indicating the day number.
1920
1921 See Also
1922 --------
1923 Series.dt.weekday : Alias.
1924 Series.dt.day_name : Returns the name of the day of the week.
1925
1926 Examples
1927 --------
1928 >>> s = pd.date_range('2016-12-31', '2017-01-08', freq='D').to_series()
1929 >>> s.dt.day_of_week
1930 2016-12-31 5
1931 2017-01-01 6
1932 2017-01-02 0
1933 2017-01-03 1
1934 2017-01-04 2
1935 2017-01-05 3
1936 2017-01-06 4
1937 2017-01-07 5
1938 2017-01-08 6
1939 Freq: D, dtype: int32
1940 """
1941 day_of_week = _field_accessor("day_of_week", "dow", _dayofweek_doc)
1942
1943 @property
1944 def weekday(self):
1945 """
1946 The day of the week with Monday=0, Sunday=6.
1947
1948 .. deprecated:: 3.1.0
1949 Use :attr:`DatetimeArray.day_of_week` instead.
1950 """
1951 # GH#12816
1952 warnings.warn(
1953 "DatetimeArray.weekday is deprecated and will be removed "
1954 "in a future version. Use DatetimeArray.day_of_week or "
1955 "Series.dt.day_of_week instead.",
1956 Pandas4Warning,
1957 stacklevel=find_stack_level(),
1958 )
1959 return self.day_of_week
1960
1961 @property
1962 def dayofweek(self):
1963 """
1964 The day of the week with Monday=0, Sunday=6.
1965
1966 .. deprecated:: 3.1.0
1967 Use :attr:`DatetimeArray.day_of_week` instead.
1968 """
1969 warnings.warn(
1970 "DatetimeArray.dayofweek is deprecated and will be removed in a "
1971 "future version. Use DatetimeArray.day_of_week instead.",
1972 Pandas4Warning,
1973 stacklevel=find_stack_level(),
1974 )
1975 return self.day_of_week
1976
1977 day_of_year = _field_accessor(
1978 "dayofyear",
1979 "doy",
1980 """
1981 The ordinal day of the year.
1982
1983 This attribute returns the day of the year for each datetime value
1984 in the Series/Index. Values range from 1 to 365 (or 366 for leap years).
1985
1986 See Also
1987 --------
1988 DatetimeIndex.dayofweek : The day of the week with Monday=0, Sunday=6.
1989 DatetimeIndex.day : The day of the datetime.
1990
1991 Examples
1992 --------
1993 For Series:
1994
1995 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
1996 >>> s = pd.to_datetime(s)
1997 >>> s
1998 0 2020-01-01 10:00:00+00:00
1999 1 2020-02-01 11:00:00+00:00
2000 dtype: datetime64[us, UTC]
2001 >>> s.dt.day_of_year
2002 0 1
2003 1 32
2004 dtype: int32
2005
2006 For DatetimeIndex:
2007
2008 >>> idx = pd.DatetimeIndex(["1/1/2020 10:00:00+00:00",
2009 ... "2/1/2020 11:00:00+00:00"])
2010 >>> idx.day_of_year
2011 Index([1, 32], dtype='int32')
2012 """,
2013 )
2014
2015 @property
2016 def dayofyear(self):
2017 """
2018 The ordinal day of the year.
2019
2020 .. deprecated:: 3.1.0
2021 Use :attr:`DatetimeArray.day_of_year` instead.
2022 """
2023 warnings.warn(
2024 "DatetimeArray.dayofyear is deprecated and will be removed in a "
2025 "future version. Use DatetimeArray.day_of_year instead.",
2026 Pandas4Warning,
2027 stacklevel=find_stack_level(),
2028 )
2029 return self.day_of_year
2030
2031 quarter = _field_accessor(
2032 "quarter",
2033 "q",
2034 """
2035 The quarter of the date.
2036
2037 This attribute returns the quarter of the year for each datetime value
2038 in the Series/Index. Quarter 1 includes January through March, quarter 2
2039 includes April through June, quarter 3 includes July through September,
2040 and quarter 4 includes October through December.
2041
2042 See Also
2043 --------
2044 DatetimeIndex.snap : Snap time stamps to nearest occurring frequency.
2045 DatetimeIndex.time : Returns numpy array of datetime.time objects.
2046 The time part of the Timestamps.
2047
2048 Examples
2049 --------
2050 For Series:
2051
2052 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "4/1/2020 11:00:00+00:00"])
2053 >>> s = pd.to_datetime(s)
2054 >>> s
2055 0 2020-01-01 10:00:00+00:00
2056 1 2020-04-01 11:00:00+00:00
2057 dtype: datetime64[us, UTC]
2058 >>> s.dt.quarter
2059 0 1
2060 1 2
2061 dtype: int32
2062
2063 For DatetimeIndex:
2064
2065 >>> idx = pd.DatetimeIndex(["1/1/2020 10:00:00+00:00",
2066 ... "2/1/2020 11:00:00+00:00"])
2067 >>> idx.quarter
2068 Index([1, 1], dtype='int32')
2069 """,
2070 )
2071 days_in_month = _field_accessor(
2072 "days_in_month",
2073 "dim",
2074 """
2075 The number of days in the month.
2076
2077 This attribute returns the total number of days in the month for each
2078 datetime value in the Series/Index. The value depends on the month and
2079 whether the year is a leap year (e.g., February has 29 days in a leap year).
2080
2081 See Also
2082 --------
2083 Series.dt.day : Return the day of the month.
2084 Series.dt.is_month_end : Return a boolean indicating if the
2085 date is the last day of the month.
2086 Series.dt.is_month_start : Return a boolean indicating if the
2087 date is the first day of the month.
2088 Series.dt.month : Return the month as January=1 through December=12.
2089
2090 Examples
2091 --------
2092 >>> s = pd.Series(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
2093 >>> s = pd.to_datetime(s)
2094 >>> s
2095 0 2020-01-01 10:00:00+00:00
2096 1 2020-02-01 11:00:00+00:00
2097 dtype: datetime64[us, UTC]
2098 >>> s.dt.days_in_month
2099 0 31
2100 1 29
2101 dtype: int32
2102 """,
2103 )
2104
2105 @property
2106 def daysinmonth(self):
2107 """
2108 The number of days in the month.
2109
2110 .. deprecated:: 3.1.0
2111 Use :attr:`DatetimeArray.days_in_month` instead.
2112 """
2113 warnings.warn(
2114 "DatetimeArray.daysinmonth is deprecated and will be removed in a "
2115 "future version. Use DatetimeArray.days_in_month instead.",
2116 Pandas4Warning,
2117 stacklevel=find_stack_level(),
2118 )
2119 return self.days_in_month
2120
2121 def _get_start_end_field(self, field: str, freq: BaseOffset | None) -> np.ndarray:
2122 """
2123 Return boolean array for is_month_start, is_quarter_end, etc.
2124
2125 Parameters
2126 ----------
2127 field : str
2128 freq : BaseOffset or None
2129 """
2130 month_kw = 12
2131 if freq:
2132 kwds = freq.kwds
2133 month_kw = kwds.get("startingMonth", kwds.get("month", month_kw))
2134
2135 if freq is not None:
2136 freq_name = freq.rule_code
2137 else:
2138 freq_name = None
2139
2140 values = self._local_timestamps()
2141 return fields.get_start_end_field(
2142 values, field, freq_name, month_kw, reso=self._creso
2143 )
2144
2145 _is_month_doc = """
2146 Indicates whether the date is the {first_or_last} day of the month.
2147
2148 This boolean attribute evaluates to True if the date falls on the
2149 {first_or_last} day of a calendar month, and False otherwise.
2150
2151 Returns
2152 -------
2153 Series or array
2154 For Series, returns a Series with boolean values.
2155 For DatetimeIndex, returns a boolean array.
2156
2157 See Also
2158 --------
2159 is_month_start : Return a boolean indicating whether the date
2160 is the first day of the month.
2161 is_month_end : Return a boolean indicating whether the date
2162 is the last day of the month.
2163
2164 Examples
2165 --------
2166 This method is available on Series with datetime values under
2167 the ``.dt`` accessor, and directly on DatetimeIndex.
2168
2169 >>> s = pd.Series(pd.date_range("2018-02-27", periods=3))
2170 >>> s
2171 0 2018-02-27
2172 1 2018-02-28
2173 2 2018-03-01
2174 dtype: datetime64[us]
2175 >>> s.dt.is_month_start
2176 0 False
2177 1 False
2178 2 True
2179 dtype: bool
2180 >>> s.dt.is_month_end
2181 0 False
2182 1 True
2183 2 False
2184 dtype: bool
2185
2186 >>> idx = pd.date_range("2018-02-27", periods=3)
2187 >>> idx.is_month_start
2188 array([False, False, True])
2189 >>> idx.is_month_end
2190 array([False, True, False])
2191 """
2192 is_month_start = _field_accessor(
2193 "is_month_start", "is_month_start", _is_month_doc.format(first_or_last="first")
2194 )
2195
2196 is_month_end = _field_accessor(
2197 "is_month_end", "is_month_end", _is_month_doc.format(first_or_last="last")
2198 )
2199
2200 is_quarter_start = _field_accessor(
2201 "is_quarter_start",
2202 "is_quarter_start",
2203 """
2204 Indicator for whether the date is the first day of a quarter.
2205
2206 This boolean attribute evaluates to True if the date falls on the
2207 first day of a calendar quarter (January 1, April 1, July 1, or
2208 October 1), and False otherwise.
2209
2210 Returns
2211 -------
2212 is_quarter_start : Series or DatetimeIndex
2213 The same type as the original data with boolean values. Series will
2214 have the same name and index. DatetimeIndex will have the same
2215 name.
2216
2217 See Also
2218 --------
2219 quarter : Return the quarter of the date.
2220 is_quarter_end : Similar property for indicating the quarter end.
2221
2222 Examples
2223 --------
2224 This method is available on Series with datetime values under
2225 the ``.dt`` accessor, and directly on DatetimeIndex.
2226
2227 >>> df = pd.DataFrame({'dates': pd.date_range("2017-03-30",
2228 ... periods=4)})
2229 >>> df.assign(quarter=df.dates.dt.quarter,
2230 ... is_quarter_start=df.dates.dt.is_quarter_start)
2231 dates quarter is_quarter_start
2232 0 2017-03-30 1 False
2233 1 2017-03-31 1 False
2234 2 2017-04-01 2 True
2235 3 2017-04-02 2 False
2236
2237 >>> idx = pd.date_range('2017-03-30', periods=4)
2238 >>> idx
2239 DatetimeIndex(['2017-03-30', '2017-03-31', '2017-04-01', '2017-04-02'],
2240 dtype='datetime64[us]', freq='D')
2241
2242 >>> idx.is_quarter_start
2243 array([False, False, True, False])
2244 """,
2245 )
2246 is_quarter_end = _field_accessor(
2247 "is_quarter_end",
2248 "is_quarter_end",
2249 """
2250 Indicator for whether the date is the last day of a quarter.
2251
2252 This boolean attribute evaluates to True if the date falls on the
2253 last day of a calendar quarter (March 31, June 30, September 30, or
2254 December 31), and False otherwise.
2255
2256 Returns
2257 -------
2258 is_quarter_end : Series or DatetimeIndex
2259 The same type as the original data with boolean values. Series will
2260 have the same name and index. DatetimeIndex will have the same
2261 name.
2262
2263 See Also
2264 --------
2265 quarter : Return the quarter of the date.
2266 is_quarter_start : Similar property indicating the quarter start.
2267
2268 Examples
2269 --------
2270 This method is available on Series with datetime values under
2271 the ``.dt`` accessor, and directly on DatetimeIndex.
2272
2273 >>> df = pd.DataFrame({'dates': pd.date_range("2017-03-30",
2274 ... periods=4)})
2275 >>> df.assign(quarter=df.dates.dt.quarter,
2276 ... is_quarter_end=df.dates.dt.is_quarter_end)
2277 dates quarter is_quarter_end
2278 0 2017-03-30 1 False
2279 1 2017-03-31 1 True
2280 2 2017-04-01 2 False
2281 3 2017-04-02 2 False
2282
2283 >>> idx = pd.date_range('2017-03-30', periods=4)
2284 >>> idx
2285 DatetimeIndex(['2017-03-30', '2017-03-31', '2017-04-01', '2017-04-02'],
2286 dtype='datetime64[us]', freq='D')
2287
2288 >>> idx.is_quarter_end
2289 array([False, True, False, False])
2290 """,
2291 )
2292 is_year_start = _field_accessor(
2293 "is_year_start",
2294 "is_year_start",
2295 """
2296 Indicate whether the date is the first day of a year.
2297
2298 This boolean attribute evaluates to True if the date is January 1st,
2299 and False otherwise.
2300
2301 Returns
2302 -------
2303 Series or DatetimeIndex
2304 The same type as the original data with boolean values. Series will
2305 have the same name and index. DatetimeIndex will have the same
2306 name.
2307
2308 See Also
2309 --------
2310 is_year_end : Similar property indicating the last day of the year.
2311
2312 Examples
2313 --------
2314 This method is available on Series with datetime values under
2315 the ``.dt`` accessor, and directly on DatetimeIndex.
2316
2317 >>> dates = pd.Series(pd.date_range("2017-12-30", periods=3))
2318 >>> dates
2319 0 2017-12-30
2320 1 2017-12-31
2321 2 2018-01-01
2322 dtype: datetime64[us]
2323
2324 >>> dates.dt.is_year_start
2325 0 False
2326 1 False
2327 2 True
2328 dtype: bool
2329
2330 >>> idx = pd.date_range("2017-12-30", periods=3)
2331 >>> idx
2332 DatetimeIndex(['2017-12-30', '2017-12-31', '2018-01-01'],
2333 dtype='datetime64[us]', freq='D')
2334
2335 >>> idx.is_year_start
2336 array([False, False, True])
2337
2338 This method, when applied to Series with datetime values under
2339 the ``.dt`` accessor, will lose information about Business offsets.
2340
2341 >>> dates = pd.Series(pd.date_range("2020-10-30", periods=4, freq="BYS"))
2342 >>> dates
2343 0 2021-01-01
2344 1 2022-01-03
2345 2 2023-01-02
2346 3 2024-01-01
2347 dtype: datetime64[us]
2348
2349 >>> dates.dt.is_year_start
2350 0 True
2351 1 False
2352 2 False
2353 3 True
2354 dtype: bool
2355
2356 >>> idx = pd.date_range("2020-10-30", periods=4, freq="BYS")
2357 >>> idx
2358 DatetimeIndex(['2021-01-01', '2022-01-03', '2023-01-02', '2024-01-01'],
2359 dtype='datetime64[us]', freq='BYS-JAN')
2360
2361 >>> idx.is_year_start
2362 array([ True, True, True, True])
2363 """,
2364 )
2365 is_year_end = _field_accessor(
2366 "is_year_end",
2367 "is_year_end",
2368 """
2369 Indicate whether the date is the last day of the year.
2370
2371 This boolean attribute evaluates to True if the date is December 31st,
2372 and False otherwise.
2373
2374 Returns
2375 -------
2376 Series or DatetimeIndex
2377 The same type as the original data with boolean values. Series will
2378 have the same name and index. DatetimeIndex will have the same
2379 name.
2380
2381 See Also
2382 --------
2383 is_year_start : Similar property indicating the start of the year.
2384
2385 Examples
2386 --------
2387 This method is available on Series with datetime values under
2388 the ``.dt`` accessor, and directly on DatetimeIndex.
2389
2390 >>> dates = pd.Series(pd.date_range("2017-12-30", periods=3))
2391 >>> dates
2392 0 2017-12-30
2393 1 2017-12-31
2394 2 2018-01-01
2395 dtype: datetime64[us]
2396
2397 >>> dates.dt.is_year_end
2398 0 False
2399 1 True
2400 2 False
2401 dtype: bool
2402
2403 >>> idx = pd.date_range("2017-12-30", periods=3)
2404 >>> idx
2405 DatetimeIndex(['2017-12-30', '2017-12-31', '2018-01-01'],
2406 dtype='datetime64[us]', freq='D')
2407
2408 >>> idx.is_year_end
2409 array([False, True, False])
2410 """,
2411 )
2412 is_leap_year = _field_accessor(
2413 "is_leap_year",
2414 "is_leap_year",
2415 """
2416 Boolean indicator if the date belongs to a leap year.
2417
2418 A leap year is a year, which has 366 days (instead of 365) including
2419 29th of February as an intercalary day.
2420 Leap years are years which are multiples of four with the exception
2421 of years divisible by 100 but not by 400.
2422
2423 Returns
2424 -------
2425 Series or ndarray
2426 Booleans indicating if dates belong to a leap year.
2427
2428 See Also
2429 --------
2430 DatetimeIndex.is_year_end : Indicate whether the date is the
2431 last day of the year.
2432 DatetimeIndex.is_year_start : Indicate whether the date is the first
2433 day of a year.
2434
2435 Examples
2436 --------
2437 This method is available on Series with datetime values under
2438 the ``.dt`` accessor, and directly on DatetimeIndex.
2439
2440 >>> idx = pd.date_range("2012-01-01", "2015-01-01", freq="YE")
2441 >>> idx
2442 DatetimeIndex(['2012-12-31', '2013-12-31', '2014-12-31'],
2443 dtype='datetime64[us]', freq='YE-DEC')
2444 >>> idx.is_leap_year
2445 array([ True, False, False])
2446
2447 >>> dates_series = pd.Series(idx)
2448 >>> dates_series
2449 0 2012-12-31
2450 1 2013-12-31
2451 2 2014-12-31
2452 dtype: datetime64[us]
2453 >>> dates_series.dt.is_leap_year
2454 0 True
2455 1 False
2456 2 False
2457 dtype: bool
2458 """,
2459 )
2460
2461 def to_julian_date(self) -> npt.NDArray[np.float64]:
2462 """
2463 Convert Timestamp to a Julian Date.
2464
2465 This method returns the number of days as a float since noon January 1, 4713 BC.
2466
2467 https://en.wikipedia.org/wiki/Julian_day
2468
2469 Returns
2470 -------
2471 ndarray or Index
2472 Float values that represent each date in Julian Calendar.
2473
2474 See Also
2475 --------
2476 Timestamp.to_julian_date : Equivalent method on ``Timestamp`` objects.
2477
2478 Examples
2479 --------
2480 >>> idx = pd.DatetimeIndex(["2028-08-12 00:54", "2028-08-12 02:06"])
2481 >>> idx.to_julian_date()
2482 Index([2461995.5375, 2461995.5875], dtype='float64')
2483 """
2484 if self.tz is not None:
2485 # GH#54763 JD is absolute-time, so use the UTC instant
2486 return self.tz_convert("UTC").tz_localize(None).to_julian_date()
2487
2488 # http://mysite.verizon.net/aesir_research/date/jdalg2.htm
2489 year = np.asarray(self.year)
2490 month = np.asarray(self.month)
2491 day = np.asarray(self.day)
2492 testarr = month < 3
2493 year[testarr] -= 1
2494 month[testarr] += 12
2495 return (
2496 day
2497 + np.trunc((153 * month - 457) / 5)
2498 + 365 * year
2499 + np.floor(year / 4)
2500 - np.floor(year / 100)
2501 + np.floor(year / 400)
2502 + 1_721_118.5
2503 + (
2504 self.hour
2505 + self.minute / 60
2506 + self.second / 3600
2507 + self.microsecond / 3600 / 10**6
2508 + self.nanosecond / 3600 / 10**9
2509 )
2510 / 24
2511 )
2512
2513 # -----------------------------------------------------------------
2514 # Reductions
2515
2516 def _reduce(
2517 self, name: str, *, skipna: bool = True, keepdims: bool = False, **kwargs
2518 ):
2519 result = super()._reduce(name, skipna=skipna, keepdims=keepdims, **kwargs)
2520 if keepdims and isinstance(result, np.ndarray):
2521 if name == "std":
2522 from pandas.core.arrays import TimedeltaArray
2523
2524 return TimedeltaArray._from_sequence(result)
2525 else:
2526 return self._from_sequence(result, dtype=self.dtype)
2527 return result
2528
2529 def std(
2530 self,
2531 axis=None,
2532 dtype=None,
2533 out=None,
2534 ddof: int = 1,
2535 keepdims: bool = False,
2536 skipna: bool = True,
2537 ) -> Timedelta:
2538 """
2539 Return sample standard deviation over requested axis.
2540
2541 Normalized by `N-1` by default. This can be changed using ``ddof``.
2542
2543 Parameters
2544 ----------
2545 axis : int, optional
2546 Axis for the function to be applied on. For :class:`pandas.Series`
2547 this parameter is unused and defaults to ``None``.
2548 dtype : dtype, optional, default None
2549 Type to use in computing the standard deviation. For arrays of
2550 integer type the default is float64, for arrays of float types
2551 it is the same as the array type.
2552 out : ndarray, optional, default None
2553 Alternative output array in which to place the result. It must have
2554 the same shape as the expected output but the type (of the
2555 calculated values) will be cast if necessary.
2556 ddof : int, default 1
2557 Degrees of Freedom. The divisor used in calculations is `N - ddof`,
2558 where `N` represents the number of elements.
2559 keepdims : bool, optional
2560 If this is set to True, the axes which are reduced are left in the
2561 result as dimensions with size one. With this option, the result
2562 will broadcast correctly against the input array. If the default
2563 value is passed, then keepdims will not be passed through to the
2564 std method of sub-classes of ndarray, however any non-default value
2565 will be. If the sub-class method does not implement keepdims any
2566 exceptions will be raised.
2567 skipna : bool, default True
2568 Exclude NA/null values. If an entire row/column is ``NA``, the result
2569 will be ``NA``.
2570
2571 Returns
2572 -------
2573 Timedelta
2574 Standard deviation over requested axis.
2575
2576 See Also
2577 --------
2578 numpy.ndarray.std : Returns the standard deviation of the array elements
2579 along given axis.
2580 Series.std : Return sample standard deviation over requested axis.
2581
2582 Examples
2583 --------
2584 For :class:`pandas.DatetimeIndex`:
2585
2586 >>> idx = pd.date_range("2001-01-01 00:00", periods=3)
2587 >>> idx
2588 DatetimeIndex(['2001-01-01', '2001-01-02', '2001-01-03'],
2589 dtype='datetime64[us]', freq='D')
2590 >>> idx.std()
2591 Timedelta('1 days 00:00:00')
2592 """
2593 # Because std is translation-invariant, we can get self.std
2594 # by calculating (self - Timestamp(0)).std, and we can do it
2595 # without creating a copy by using a view on self._ndarray
2596 from pandas.core.arrays import TimedeltaArray
2597
2598 # Find the td64 dtype with the same resolution as our dt64 dtype
2599 dtype_str = self._ndarray.dtype.name.replace("datetime64", "timedelta64")
2600 dtype = np.dtype(dtype_str)
2601
2602 tda = TimedeltaArray._simple_new(self._ndarray.view(dtype), dtype=dtype)
2603
2604 return tda.std(axis=axis, out=out, ddof=ddof, keepdims=keepdims, skipna=skipna)
2605
2606
2607# -------------------------------------------------------------------
2608# Constructor Helpers
2609
2610
2611def _sequence_to_dt64(
2612 data: ArrayLike,
2613 *,
2614 copy: bool = False,
2615 tz: tzinfo | None = None,
2616 dayfirst: bool = False,
2617 yearfirst: bool = False,
2618 ambiguous: TimeAmbiguous = "raise",
2619 out_unit: str | None = None,
2620) -> tuple[np.ndarray, tzinfo | None]:
2621 """
2622 Parameters
2623 ----------
2624 data : np.ndarray or ExtensionArray
2625 dtl.ensure_arraylike_for_datetimelike has already been called.
2626 copy : bool, default False
2627 tz : tzinfo or None, default None
2628 dayfirst : bool, default False
2629 yearfirst : bool, default False
2630 ambiguous : str, bool, or arraylike, default 'raise'
2631 See pandas._libs.tslibs.tzconversion.tz_localize_to_utc.
2632 out_unit : str or None, default None
2633 Desired output resolution.
2634
2635 Returns
2636 -------
2637 result : numpy.ndarray
2638 The sequence converted to a numpy array with dtype ``datetime64[unit]``.
2639 Where `unit` is "ns" unless specified otherwise by `out_unit`.
2640 tz : tzinfo or None
2641 Either the user-provided tzinfo or one inferred from the data.
2642
2643 Raises
2644 ------
2645 TypeError : PeriodDType data is passed
2646 """
2647
2648 # By this point we are assured to have either a numpy array or Index
2649 data, copy = maybe_convert_dtype(data, copy, tz=tz)
2650 data_dtype = getattr(data, "dtype", None)
2651
2652 out_dtype = DT64NS_DTYPE
2653 if out_unit is not None:
2654 out_dtype = np.dtype(f"M8[{out_unit}]")
2655
2656 if data_dtype == object or is_string_dtype(data_dtype):
2657 # TODO: We do not have tests specific to string-dtypes,
2658 # also complex or categorical or other extension
2659 data = cast("np.ndarray", data)
2660 copy = False
2661 if lib.infer_dtype(data, skipna=False) == "integer":
2662 # Much more performant than going through array_to_datetime
2663 data = data.astype(np.int64)
2664 elif tz is not None and ambiguous == "raise":
2665 obj_data = np.asarray(data, dtype=object)
2666 result = tslib.array_to_datetime_with_tz(
2667 obj_data,
2668 tz=tz,
2669 dayfirst=dayfirst,
2670 yearfirst=yearfirst,
2671 creso=abbrev_to_npy_unit(out_unit),
2672 )
2673 return result, tz
2674 else:
2675 converted, inferred_tz = objects_to_datetime64(
2676 data,
2677 dayfirst=dayfirst,
2678 yearfirst=yearfirst,
2679 out_unit=out_unit,
2680 )
2681 copy = False
2682 if tz and inferred_tz:
2683 # two timezones: convert to intended from base UTC repr
2684 # GH#42505 by convention, these are _already_ UTC
2685 result = converted
2686
2687 elif inferred_tz:
2688 tz = inferred_tz
2689 result = converted
2690
2691 else:
2692 result, _ = _construct_from_dt64_naive(
2693 converted, tz=tz, copy=copy, ambiguous=ambiguous
2694 )
2695 return result, tz
2696
2697 data_dtype = data.dtype
2698
2699 # `data` may have originally been a Categorical[datetime64[ns, tz]],
2700 # so we need to handle these types.
2701 if isinstance(data_dtype, DatetimeTZDtype):
2702 # DatetimeArray -> ndarray
2703 data = cast("DatetimeArray", data)
2704 tz = _maybe_infer_tz(tz, data.tz)
2705 result = data._ndarray
2706
2707 elif lib.is_np_dtype(data_dtype, "M"):
2708 # tz-naive DatetimeArray or ndarray[datetime64]
2709 if isinstance(data, DatetimeArray):
2710 data = data._ndarray
2711
2712 data = cast("np.ndarray", data)
2713 result, copy = _construct_from_dt64_naive(
2714 data, tz=tz, copy=copy, ambiguous=ambiguous
2715 )
2716
2717 else:
2718 # must be integer dtype otherwise
2719 # assume this data are epoch timestamps
2720 if data.dtype != INT64_DTYPE:
2721 data = data.astype(np.int64, copy=False)
2722 copy = False
2723 data = cast("np.ndarray", data)
2724 result = data.view(out_dtype)
2725
2726 if copy:
2727 result = result.copy()
2728
2729 assert isinstance(result, np.ndarray), type(result)
2730 assert result.dtype.kind == "M"
2731 assert result.dtype != "M8"
2732 assert is_supported_dtype(result.dtype)
2733 return result, tz
2734
2735
2736def _construct_from_dt64_naive(
2737 data: np.ndarray, *, tz: tzinfo | None, copy: bool, ambiguous: TimeAmbiguous
2738) -> tuple[np.ndarray, bool]:
2739 """
2740 Convert datetime64 data to a supported dtype, localizing if necessary.
2741 """
2742 # Caller is responsible for ensuring
2743 # lib.is_np_dtype(data.dtype)
2744
2745 new_dtype = data.dtype
2746 if not is_supported_dtype(new_dtype):
2747 # Cast to the nearest supported unit, generally "s"
2748 new_dtype = get_supported_dtype(new_dtype)
2749 data = astype_overflowsafe(data, dtype=new_dtype, copy=False)
2750 copy = False
2751
2752 if data.dtype.byteorder == ">":
2753 # TODO: better way to handle this? non-copying alternative?
2754 # without this, test_constructor_datetime64_bigendian fails
2755 data = data.astype(data.dtype.newbyteorder("<"))
2756 new_dtype = data.dtype
2757 copy = False
2758
2759 if tz is not None:
2760 # Convert tz-naive to UTC
2761 # TODO: if tz is UTC, are there situations where we *don't* want a
2762 # copy? tz_localize_to_utc always makes one.
2763 shape = data.shape
2764 if data.ndim > 1:
2765 data = data.ravel()
2766
2767 data_unit = get_unit_from_dtype(new_dtype)
2768 data = tzconversion.tz_localize_to_utc(
2769 data.view("i8"), tz, ambiguous=ambiguous, creso=data_unit
2770 )
2771 data = data.view(new_dtype)
2772 data = data.reshape(shape)
2773
2774 assert data.dtype == new_dtype, data.dtype
2775 result = data
2776
2777 return result, copy
2778
2779
2780def objects_to_datetime64(
2781 data: np.ndarray,
2782 dayfirst,
2783 yearfirst,
2784 utc: bool = False,
2785 errors: DateTimeErrorChoices = "raise",
2786 out_unit: str | None = None,
2787) -> tuple[np.ndarray, tzinfo | None]:
2788 """
2789 Convert data to array of timestamps.
2790
2791 Parameters
2792 ----------
2793 data : np.ndarray[object]
2794 dayfirst : bool
2795 yearfirst : bool
2796 utc : bool, default False
2797 Whether to convert/localize timestamps to UTC.
2798 errors : {'raise', 'coerce'}
2799 out_unit : str or None, default None
2800 None indicates we should do resolution inference.
2801
2802 Returns
2803 -------
2804 result : ndarray
2805 np.datetime64[out_unit] if returned values represent wall times or UTC
2806 timestamps.
2807 inferred_tz : tzinfo or None
2808 If not None, then the datetime64 values in `result` denote UTC timestamps.
2809
2810 Raises
2811 ------
2812 ValueError : if data cannot be converted to datetimes
2813 TypeError : When a type cannot be converted to datetime
2814 """
2815 assert errors in ["raise", "coerce"]
2816
2817 # if str-dtype, convert
2818 data = np.asarray(data, dtype=np.object_)
2819
2820 result, tz_parsed = tslib.array_to_datetime(
2821 data,
2822 errors=errors,
2823 utc=utc,
2824 dayfirst=dayfirst,
2825 yearfirst=yearfirst,
2826 creso=abbrev_to_npy_unit(out_unit),
2827 )
2828
2829 if tz_parsed is not None:
2830 # We can take a shortcut since the datetime64 numpy array
2831 # is in UTC
2832 return result, tz_parsed
2833 elif result.dtype.kind == "M":
2834 return result, tz_parsed
2835 else: # pragma: no cover
2836 # this TypeError should never be hit
2837 raise TypeError(result)
2838
2839
2840def maybe_convert_dtype(data, copy: bool, tz: tzinfo | None = None):
2841 """
2842 Convert data based on dtype conventions, issuing
2843 errors where appropriate.
2844
2845 Parameters
2846 ----------
2847 data : np.ndarray or pd.Index
2848 copy : bool
2849 tz : tzinfo or None, default None
2850
2851 Returns
2852 -------
2853 data : np.ndarray or pd.Index
2854 copy : bool
2855
2856 Raises
2857 ------
2858 TypeError : PeriodDType data is passed
2859 """
2860 if not hasattr(data, "dtype"):
2861 # e.g. collections.deque
2862 return data, copy
2863
2864 if is_float_dtype(data.dtype):
2865 # pre-2.0 we treated these as wall-times, inconsistent with ints
2866 # GH#23675, GH#45573 deprecated to treat symmetrically with integer dtypes.
2867 # Note: data.astype(np.int64) fails ARM tests, see
2868 # https://github.com/pandas-dev/pandas/issues/49468.
2869 data = data.astype(DT64NS_DTYPE).view("i8")
2870 copy = False
2871
2872 elif lib.is_np_dtype(data.dtype, "m") or is_bool_dtype(data.dtype):
2873 # GH#29794 enforcing deprecation introduced in GH#23539
2874 raise TypeError(f"dtype {data.dtype} cannot be converted to datetime64[ns]")
2875 elif isinstance(data.dtype, PeriodDtype):
2876 # Note: without explicitly raising here, PeriodIndex
2877 # test_setops.test_join_does_not_recur fails
2878 raise TypeError(
2879 "Passing PeriodDtype data is invalid. Use `data.to_timestamp()` instead"
2880 )
2881
2882 elif isinstance(data.dtype, ExtensionDtype) and not isinstance(
2883 data.dtype, DatetimeTZDtype
2884 ):
2885 # TODO: We have no tests for these
2886 data = np.array(data, dtype=np.object_)
2887 copy = False
2888
2889 return data, copy
2890
2891
2892# -------------------------------------------------------------------
2893# Validation and Inference
2894
2895
2896def _maybe_infer_tz(tz: tzinfo | None, inferred_tz: tzinfo | None) -> tzinfo | None:
2897 """
2898 If a timezone is inferred from data, check that it is compatible with
2899 the user-provided timezone, if any.
2900
2901 Parameters
2902 ----------
2903 tz : tzinfo or None
2904 inferred_tz : tzinfo or None
2905
2906 Returns
2907 -------
2908 tz : tzinfo or None
2909
2910 Raises
2911 ------
2912 TypeError : if both timezones are present but do not match
2913 """
2914 if tz is None:
2915 tz = inferred_tz
2916 elif inferred_tz is None:
2917 pass
2918 elif not timezones.tz_compare(tz, inferred_tz):
2919 raise TypeError(
2920 f"data is already tz-aware {inferred_tz}, unable to set specified tz: {tz}"
2921 )
2922 return tz
2923
2924
2925def _validate_dt64_dtype(dtype):
2926 """
2927 Check that a dtype, if passed, represents either a numpy datetime64[ns]
2928 dtype or a pandas DatetimeTZDtype.
2929
2930 Parameters
2931 ----------
2932 dtype : object
2933
2934 Returns
2935 -------
2936 dtype : None, numpy.dtype, or DatetimeTZDtype
2937
2938 Raises
2939 ------
2940 ValueError : invalid dtype
2941
2942 Notes
2943 -----
2944 Unlike _validate_tz_from_dtype, this does _not_ allow non-existent
2945 tz errors to go through
2946 """
2947 if dtype is not None:
2948 dtype = pandas_dtype(dtype)
2949 if dtype == np.dtype("M8"):
2950 # no precision, disallowed GH#24806
2951 msg = (
2952 "Passing in 'datetime64' dtype with no precision is not allowed. "
2953 "Please pass in 'datetime64[ns]' instead."
2954 )
2955 raise ValueError(msg)
2956
2957 if (
2958 isinstance(dtype, np.dtype)
2959 and (dtype.kind != "M" or not is_supported_dtype(dtype))
2960 ) or not isinstance(dtype, (np.dtype, DatetimeTZDtype)):
2961 raise ValueError(
2962 f"Unexpected value for 'dtype': '{dtype}'. "
2963 "Must be 'datetime64[s]', 'datetime64[ms]', 'datetime64[us]', "
2964 "'datetime64[ns]' or DatetimeTZDtype'."
2965 )
2966
2967 if getattr(dtype, "tz", None):
2968 # https://github.com/pandas-dev/pandas/issues/18595
2969 # Ensure that we have a standard timezone for pytz objects.
2970 # Without this, things like adding an array of timedeltas and
2971 # a tz-aware Timestamp (with a tz specific to its datetime) will
2972 # be incorrect(ish?) for the array as a whole
2973 dtype = cast("DatetimeTZDtype", dtype)
2974 dtype = DatetimeTZDtype(
2975 unit=dtype.unit, tz=timezones.tz_standardize(dtype.tz)
2976 )
2977
2978 return dtype
2979
2980
2981def _validate_tz_from_dtype(
2982 dtype, tz: tzinfo | None, explicit_tz_none: bool = False
2983) -> tzinfo | None:
2984 """
2985 If the given dtype is a DatetimeTZDtype, extract the implied
2986 tzinfo object from it and check that it does not conflict with the given
2987 tz.
2988
2989 Parameters
2990 ----------
2991 dtype : dtype, str
2992 tz : None, tzinfo
2993 explicit_tz_none : bool, default False
2994 Whether tz=None was passed explicitly, as opposed to lib.no_default.
2995
2996 Returns
2997 -------
2998 tz : consensus tzinfo
2999
3000 Raises
3001 ------
3002 ValueError : on tzinfo mismatch
3003 """
3004 if dtype is not None:
3005 if isinstance(dtype, str):
3006 try:
3007 dtype = DatetimeTZDtype.construct_from_string(dtype)
3008 except TypeError:
3009 # Things like `datetime64[ns]`, which is OK for the
3010 # constructors, but also nonsense, which should be validated
3011 # but not by us. We *do* allow non-existent tz errors to
3012 # go through
3013 pass
3014 dtz = getattr(dtype, "tz", None)
3015 if dtz is not None:
3016 if tz is not None and not timezones.tz_compare(tz, dtz):
3017 raise ValueError("cannot supply both a tz and a dtype with a tz")
3018 if explicit_tz_none:
3019 raise ValueError("Cannot pass both a timezone-aware dtype and tz=None")
3020 tz = dtz
3021
3022 if tz is not None and lib.is_np_dtype(dtype, "M"):
3023 # We also need to check for the case where the user passed a
3024 # tz-naive dtype (i.e. datetime64[ns])
3025 if tz is not None and not timezones.tz_compare(tz, dtz):
3026 raise ValueError(
3027 "cannot supply both a tz and a "
3028 "timezone-naive dtype (i.e. datetime64[ns])"
3029 )
3030
3031 return tz
3032
3033
3034def _infer_tz_from_endpoints(
3035 start: Timestamp, end: Timestamp, tz: tzinfo | None
3036) -> tzinfo | None:
3037 """
3038 If a timezone is not explicitly given via `tz`, see if one can
3039 be inferred from the `start` and `end` endpoints. If more than one
3040 of these inputs provides a timezone, require that they all agree.
3041
3042 Parameters
3043 ----------
3044 start : Timestamp
3045 end : Timestamp
3046 tz : tzinfo or None
3047
3048 Returns
3049 -------
3050 tz : tzinfo or None
3051
3052 Raises
3053 ------
3054 TypeError : if start and end timezones do not agree
3055 """
3056 try:
3057 inferred_tz = timezones.infer_tzinfo(start, end)
3058 except AssertionError as err:
3059 # infer_tzinfo raises AssertionError if passed mismatched timezones
3060 raise TypeError(
3061 "Start and end cannot both be tz-aware with different timezones"
3062 ) from err
3063
3064 inferred_tz = timezones.maybe_get_tz(inferred_tz)
3065 tz = timezones.maybe_get_tz(tz)
3066
3067 if tz is not None and inferred_tz is not None:
3068 if not timezones.tz_compare(inferred_tz, tz):
3069 raise AssertionError("Inferred time zone not equal to passed time zone")
3070
3071 elif inferred_tz is not None:
3072 tz = inferred_tz
3073
3074 return tz
3075
3076
3077def _maybe_normalize_endpoints(
3078 start: _TimestampNoneT1, end: _TimestampNoneT2, normalize: bool
3079) -> tuple[_TimestampNoneT1, _TimestampNoneT2]:
3080 if normalize:
3081 if start is not None:
3082 start = start.normalize()
3083
3084 if end is not None:
3085 end = end.normalize()
3086
3087 return start, end
3088
3089
3090def _maybe_localize_point(
3091 ts: Timestamp | None, freq, tz, ambiguous, nonexistent
3092) -> Timestamp | None:
3093 """
3094 Localize a start or end Timestamp to the timezone of the corresponding
3095 start or end Timestamp
3096
3097 Parameters
3098 ----------
3099 ts : start or end Timestamp to potentially localize
3100 freq : Tick, DateOffset, or None
3101 tz : str, timezone object or None
3102 ambiguous: str, localization behavior for ambiguous times
3103 nonexistent: str, localization behavior for nonexistent times
3104
3105 Returns
3106 -------
3107 ts : Timestamp
3108 """
3109 # Make sure start and end are timezone localized if:
3110 # 1) freq = a Timedelta-like frequency (Tick)
3111 # 2) freq = None i.e. generating a linspaced range
3112 if ts is not None and ts.tzinfo is None:
3113 # Note: We can't ambiguous='infer' a singular ambiguous time; however,
3114 # we have historically defaulted ambiguous=False
3115 ambiguous = ambiguous if ambiguous != "infer" else False
3116 localize_args = {"ambiguous": ambiguous, "nonexistent": nonexistent, "tz": None}
3117 if isinstance(freq, Tick) or freq is None:
3118 localize_args["tz"] = tz
3119 ts = ts.tz_localize(**localize_args)
3120 return ts
3121
3122
3123def _generate_range(
3124 start: Timestamp | None,
3125 end: Timestamp | None,
3126 periods: int | None,
3127 offset: BaseOffset,
3128 *,
3129 unit: TimeUnit,
3130) -> Generator[Timestamp]:
3131 """
3132 Generates a sequence of dates corresponding to the specified time
3133 offset. Similar to dateutil.rrule except uses pandas DateOffset
3134 objects to represent time increments.
3135
3136 Parameters
3137 ----------
3138 start : Timestamp or None
3139 end : Timestamp or None
3140 periods : int or None
3141 offset : DateOffset
3142 unit : str
3143
3144 Notes
3145 -----
3146 * This method is faster for generating weekdays than dateutil.rrule
3147 * At least two of (start, end, periods) must be specified.
3148 * If both start and end are specified, the returned dates will
3149 satisfy start <= date <= end.
3150
3151 Returns
3152 -------
3153 dates : generator object
3154 """
3155 offset = to_offset(offset)
3156
3157 start = Timestamp(start)
3158 # error: Non-overlapping identity check (left operand type: "Timestamp",
3159 # right operand type: "NaTType")
3160 if start is not NaT: # type: ignore[comparison-overlap]
3161 start = start.as_unit(unit)
3162 else:
3163 start = None
3164
3165 end = Timestamp(end)
3166 # error: Non-overlapping identity check (left operand type: "Timestamp",
3167 # right operand type: "NaTType")
3168 if end is not NaT: # type: ignore[comparison-overlap]
3169 end = end.as_unit(unit)
3170 else:
3171 end = None
3172
3173 # GH#64834/GH#65011 When deriving start from (end, periods), roll end onto
3174 # the offset first so we don't lose a period. For B/bh this is handled by
3175 # the vectorized path, but anchored offsets (W, ME, MS, QS, ...) reach here.
3176 # Without this, periods=1 also leaves freq pinned to an off-grid end.
3177 if end is not None and periods is not None and not offset.is_on_offset(end):
3178 if offset.n >= 0:
3179 end = offset.rollback(end) # type: ignore[assignment]
3180 else:
3181 end = offset.rollforward(end) # type: ignore[assignment]
3182
3183 if start and not offset.is_on_offset(start):
3184 # Incompatible types in assignment (expression has type "datetime",
3185 # variable has type "Optional[Timestamp]")
3186
3187 # GH #56147 account for negative direction and range bounds
3188 if offset.n >= 0:
3189 start = offset.rollforward(start) # type: ignore[assignment]
3190 else:
3191 start = offset.rollback(start) # type: ignore[assignment]
3192
3193 # Unsupported operand types for < ("Timestamp" and "None")
3194 if periods is None and end < start and offset.n >= 0: # type: ignore[operator]
3195 end = None
3196 periods = 0
3197
3198 if end is None:
3199 # GH#41563 avoid 0 * offset for offsets where n=0 is not allowed
3200 if periods == 1:
3201 end = start
3202 else:
3203 # error: No overload variant of "__radd__" of "BaseOffset" matches
3204 # argument type "None"
3205 end = start + (periods - 1) * offset # type: ignore[operator]
3206
3207 if start is None:
3208 # GH#41563 avoid 0 * offset for offsets where n=0 is not allowed
3209 if periods == 1:
3210 start = end
3211 else:
3212 # error: No overload variant of "__radd__" of "BaseOffset" matches
3213 # argument type "None"
3214 start = end - (periods - 1) * offset # type: ignore[operator]
3215
3216 start = cast("Timestamp", start)
3217 end = cast("Timestamp", end)
3218
3219 cur = start
3220 if offset.n >= 0:
3221 while cur <= end:
3222 yield cur
3223
3224 if cur == end:
3225 # GH#24252 avoid overflows by not performing the addition
3226 # in offset.apply unless we have to
3227 break
3228
3229 # faster than cur + offset
3230 next_date = offset._add_datetime(cur)
3231 next_date = next_date.as_unit(unit)
3232 if next_date <= cur:
3233 raise ValueError(f"Offset {offset} did not increment date")
3234 cur = next_date
3235 else:
3236 while cur >= end:
3237 yield cur
3238
3239 if cur == end:
3240 # GH#24252 avoid overflows by not performing the addition
3241 # in offset.apply unless we have to
3242 break
3243
3244 # faster than cur + offset
3245 next_date = offset._add_datetime(cur)
3246 next_date = next_date.as_unit(unit)
3247 if next_date >= cur:
3248 raise ValueError(f"Offset {offset} did not decrement date")
3249 cur = next_date