1from __future__ import annotations
2
3import datetime as dt
4import operator
5from typing import (
6 TYPE_CHECKING,
7 Hashable,
8)
9import warnings
10
11import numpy as np
12import pytz
13
14from pandas._libs import (
15 NaT,
16 Period,
17 Timestamp,
18 index as libindex,
19 lib,
20)
21from pandas._libs.tslibs import (
22 Resolution,
23 periods_per_day,
24 timezones,
25 to_offset,
26)
27from pandas._libs.tslibs.offsets import prefix_mapping
28from pandas._typing import (
29 Dtype,
30 DtypeObj,
31 Frequency,
32 IntervalClosedType,
33 TimeAmbiguous,
34 TimeNonexistent,
35 npt,
36)
37from pandas.util._decorators import (
38 cache_readonly,
39 doc,
40)
41
42from pandas.core.dtypes.common import (
43 is_datetime64_dtype,
44 is_datetime64tz_dtype,
45 is_scalar,
46)
47from pandas.core.dtypes.generic import ABCSeries
48from pandas.core.dtypes.missing import is_valid_na_for_dtype
49
50from pandas.core.arrays.datetimes import (
51 DatetimeArray,
52 tz_to_dtype,
53)
54import pandas.core.common as com
55from pandas.core.indexes.base import (
56 Index,
57 maybe_extract_name,
58)
59from pandas.core.indexes.datetimelike import DatetimeTimedeltaMixin
60from pandas.core.indexes.extension import inherit_names
61from pandas.core.tools.times import to_time
62
63if TYPE_CHECKING:
64 from pandas.core.api import (
65 DataFrame,
66 PeriodIndex,
67 )
68
69
70def _new_DatetimeIndex(cls, d):
71 """
72 This is called upon unpickling, rather than the default which doesn't
73 have arguments and breaks __new__
74 """
75 if "data" in d and not isinstance(d["data"], DatetimeIndex):
76 # Avoid need to verify integrity by calling simple_new directly
77 data = d.pop("data")
78 if not isinstance(data, DatetimeArray):
79 # For backward compat with older pickles, we may need to construct
80 # a DatetimeArray to adapt to the newer _simple_new signature
81 tz = d.pop("tz")
82 freq = d.pop("freq")
83 dta = DatetimeArray._simple_new(data, dtype=tz_to_dtype(tz), freq=freq)
84 else:
85 dta = data
86 for key in ["tz", "freq"]:
87 # These are already stored in our DatetimeArray; if they are
88 # also in the pickle and don't match, we have a problem.
89 if key in d:
90 assert d[key] == getattr(dta, key)
91 d.pop(key)
92 result = cls._simple_new(dta, **d)
93 else:
94 with warnings.catch_warnings():
95 # TODO: If we knew what was going in to **d, we might be able to
96 # go through _simple_new instead
97 warnings.simplefilter("ignore")
98 result = cls.__new__(cls, **d)
99
100 return result
101
102
103@inherit_names(
104 DatetimeArray._field_ops
105 + [
106 method
107 for method in DatetimeArray._datetimelike_methods
108 if method not in ("tz_localize", "tz_convert", "strftime")
109 ],
110 DatetimeArray,
111 wrap=True,
112)
113@inherit_names(["is_normalized"], DatetimeArray, cache=True)
114@inherit_names(
115 [
116 "tz",
117 "tzinfo",
118 "dtype",
119 "to_pydatetime",
120 "_format_native_types",
121 "date",
122 "time",
123 "timetz",
124 "std",
125 ]
126 + DatetimeArray._bool_ops,
127 DatetimeArray,
128)
129class DatetimeIndex(DatetimeTimedeltaMixin):
130 """
131 Immutable ndarray-like of datetime64 data.
132
133 Represented internally as int64, and which can be boxed to Timestamp objects
134 that are subclasses of datetime and carry metadata.
135
136 .. versionchanged:: 2.0.0
137 The various numeric date/time attributes (:attr:`~DatetimeIndex.day`,
138 :attr:`~DatetimeIndex.month`, :attr:`~DatetimeIndex.year` etc.) now have dtype
139 ``int32``. Previously they had dtype ``int64``.
140
141 Parameters
142 ----------
143 data : array-like (1-dimensional)
144 Datetime-like data to construct index with.
145 freq : str or pandas offset object, optional
146 One of pandas date offset strings or corresponding objects. The string
147 'infer' can be passed in order to set the frequency of the index as the
148 inferred frequency upon creation.
149 tz : pytz.timezone or dateutil.tz.tzfile or datetime.tzinfo or str
150 Set the Timezone of the data.
151 normalize : bool, default False
152 Normalize start/end dates to midnight before generating date range.
153 closed : {'left', 'right'}, optional
154 Set whether to include `start` and `end` that are on the
155 boundary. The default includes boundary points on either end.
156 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
157 When clocks moved backward due to DST, ambiguous times may arise.
158 For example in Central European Time (UTC+01), when going from 03:00
159 DST to 02:00 non-DST, 02:30:00 local time occurs both at 00:30:00 UTC
160 and at 01:30:00 UTC. In such a situation, the `ambiguous` parameter
161 dictates how ambiguous times should be handled.
162
163 - 'infer' will attempt to infer fall dst-transition hours based on
164 order
165 - bool-ndarray where True signifies a DST time, False signifies a
166 non-DST time (note that this flag is only applicable for ambiguous
167 times)
168 - 'NaT' will return NaT where there are ambiguous times
169 - 'raise' will raise an AmbiguousTimeError if there are ambiguous times.
170 dayfirst : bool, default False
171 If True, parse dates in `data` with the day first order.
172 yearfirst : bool, default False
173 If True parse dates in `data` with the year first order.
174 dtype : numpy.dtype or DatetimeTZDtype or str, default None
175 Note that the only NumPy dtype allowed is ‘datetime64[ns]’.
176 copy : bool, default False
177 Make a copy of input ndarray.
178 name : label, default None
179 Name to be stored in the index.
180
181 Attributes
182 ----------
183 year
184 month
185 day
186 hour
187 minute
188 second
189 microsecond
190 nanosecond
191 date
192 time
193 timetz
194 dayofyear
195 day_of_year
196 weekofyear
197 week
198 dayofweek
199 day_of_week
200 weekday
201 quarter
202 tz
203 freq
204 freqstr
205 is_month_start
206 is_month_end
207 is_quarter_start
208 is_quarter_end
209 is_year_start
210 is_year_end
211 is_leap_year
212 inferred_freq
213
214 Methods
215 -------
216 normalize
217 strftime
218 snap
219 tz_convert
220 tz_localize
221 round
222 floor
223 ceil
224 to_period
225 to_pydatetime
226 to_series
227 to_frame
228 month_name
229 day_name
230 mean
231 std
232
233 See Also
234 --------
235 Index : The base pandas Index type.
236 TimedeltaIndex : Index of timedelta64 data.
237 PeriodIndex : Index of Period data.
238 to_datetime : Convert argument to datetime.
239 date_range : Create a fixed-frequency DatetimeIndex.
240
241 Notes
242 -----
243 To learn more about the frequency strings, please see `this link
244 <https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases>`__.
245 """
246
247 _typ = "datetimeindex"
248
249 _data_cls = DatetimeArray
250 _supports_partial_string_indexing = True
251
252 @property
253 def _engine_type(self) -> type[libindex.DatetimeEngine]:
254 return libindex.DatetimeEngine
255
256 _data: DatetimeArray
257 tz: dt.tzinfo | None
258
259 # --------------------------------------------------------------------
260 # methods that dispatch to DatetimeArray and wrap result
261
262 @doc(DatetimeArray.strftime)
263 def strftime(self, date_format) -> Index:
264 arr = self._data.strftime(date_format)
265 return Index(arr, name=self.name, dtype=object)
266
267 @doc(DatetimeArray.tz_convert)
268 def tz_convert(self, tz) -> DatetimeIndex:
269 arr = self._data.tz_convert(tz)
270 return type(self)._simple_new(arr, name=self.name, refs=self._references)
271
272 @doc(DatetimeArray.tz_localize)
273 def tz_localize(
274 self,
275 tz,
276 ambiguous: TimeAmbiguous = "raise",
277 nonexistent: TimeNonexistent = "raise",
278 ) -> DatetimeIndex:
279 arr = self._data.tz_localize(tz, ambiguous, nonexistent)
280 return type(self)._simple_new(arr, name=self.name)
281
282 @doc(DatetimeArray.to_period)
283 def to_period(self, freq=None) -> PeriodIndex:
284 from pandas.core.indexes.api import PeriodIndex
285
286 arr = self._data.to_period(freq)
287 return PeriodIndex._simple_new(arr, name=self.name)
288
289 @doc(DatetimeArray.to_julian_date)
290 def to_julian_date(self) -> Index:
291 arr = self._data.to_julian_date()
292 return Index._simple_new(arr, name=self.name)
293
294 @doc(DatetimeArray.isocalendar)
295 def isocalendar(self) -> DataFrame:
296 df = self._data.isocalendar()
297 return df.set_index(self)
298
299 @cache_readonly
300 def _resolution_obj(self) -> Resolution:
301 return self._data._resolution_obj
302
303 # --------------------------------------------------------------------
304 # Constructors
305
306 def __new__(
307 cls,
308 data=None,
309 freq: Frequency | lib.NoDefault = lib.no_default,
310 tz=lib.no_default,
311 normalize: bool = False,
312 closed=None,
313 ambiguous: TimeAmbiguous = "raise",
314 dayfirst: bool = False,
315 yearfirst: bool = False,
316 dtype: Dtype | None = None,
317 copy: bool = False,
318 name: Hashable = None,
319 ) -> DatetimeIndex:
320 if is_scalar(data):
321 cls._raise_scalar_data_error(data)
322
323 # - Cases checked above all return/raise before reaching here - #
324
325 name = maybe_extract_name(name, data, cls)
326
327 if (
328 isinstance(data, DatetimeArray)
329 and freq is lib.no_default
330 and tz is lib.no_default
331 and dtype is None
332 ):
333 # fastpath, similar logic in TimedeltaIndex.__new__;
334 # Note in this particular case we retain non-nano.
335 if copy:
336 data = data.copy()
337 return cls._simple_new(data, name=name)
338
339 dtarr = DatetimeArray._from_sequence_not_strict(
340 data,
341 dtype=dtype,
342 copy=copy,
343 tz=tz,
344 freq=freq,
345 dayfirst=dayfirst,
346 yearfirst=yearfirst,
347 ambiguous=ambiguous,
348 )
349 refs = None
350 if not copy and isinstance(data, (Index, ABCSeries)):
351 refs = data._references
352
353 subarr = cls._simple_new(dtarr, name=name, refs=refs)
354 return subarr
355
356 # --------------------------------------------------------------------
357
358 @cache_readonly
359 def _is_dates_only(self) -> bool:
360 """
361 Return a boolean if we are only dates (and don't have a timezone)
362
363 Returns
364 -------
365 bool
366 """
367 from pandas.io.formats.format import is_dates_only
368
369 # error: Argument 1 to "is_dates_only" has incompatible type
370 # "Union[ExtensionArray, ndarray]"; expected "Union[ndarray,
371 # DatetimeArray, Index, DatetimeIndex]"
372 return self.tz is None and is_dates_only(self._values) # type: ignore[arg-type]
373
374 def __reduce__(self):
375 d = {"data": self._data, "name": self.name}
376 return _new_DatetimeIndex, (type(self), d), None
377
378 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
379 """
380 Can we compare values of the given dtype to our own?
381 """
382 if self.tz is not None:
383 # If we have tz, we can compare to tzaware
384 return is_datetime64tz_dtype(dtype)
385 # if we dont have tz, we can only compare to tznaive
386 return is_datetime64_dtype(dtype)
387
388 # --------------------------------------------------------------------
389 # Rendering Methods
390
391 @property
392 def _formatter_func(self):
393 from pandas.io.formats.format import get_format_datetime64
394
395 formatter = get_format_datetime64(is_dates_only_=self._is_dates_only)
396 return lambda x: f"'{formatter(x)}'"
397
398 # --------------------------------------------------------------------
399 # Set Operation Methods
400
401 def _can_range_setop(self, other) -> bool:
402 # GH 46702: If self or other have non-UTC tzs, DST transitions prevent
403 # range representation due to no singular step
404 if (
405 self.tz is not None
406 and not timezones.is_utc(self.tz)
407 and not timezones.is_fixed_offset(self.tz)
408 ):
409 return False
410 if (
411 other.tz is not None
412 and not timezones.is_utc(other.tz)
413 and not timezones.is_fixed_offset(other.tz)
414 ):
415 return False
416 return super()._can_range_setop(other)
417
418 # --------------------------------------------------------------------
419
420 def _get_time_micros(self) -> npt.NDArray[np.int64]:
421 """
422 Return the number of microseconds since midnight.
423
424 Returns
425 -------
426 ndarray[int64_t]
427 """
428 values = self._data._local_timestamps()
429
430 ppd = periods_per_day(self._data._creso)
431
432 frac = values % ppd
433 if self.unit == "ns":
434 micros = frac // 1000
435 elif self.unit == "us":
436 micros = frac
437 elif self.unit == "ms":
438 micros = frac * 1000
439 elif self.unit == "s":
440 micros = frac * 1_000_000
441 else: # pragma: no cover
442 raise NotImplementedError(self.unit)
443
444 micros[self._isnan] = -1
445 return micros
446
447 def snap(self, freq: Frequency = "S") -> DatetimeIndex:
448 """
449 Snap time stamps to nearest occurring frequency.
450
451 Returns
452 -------
453 DatetimeIndex
454 """
455 # Superdumb, punting on any optimizing
456 freq = to_offset(freq)
457
458 dta = self._data.copy()
459
460 for i, v in enumerate(self):
461 s = v
462 if not freq.is_on_offset(s):
463 t0 = freq.rollback(s)
464 t1 = freq.rollforward(s)
465 if abs(s - t0) < abs(t1 - s):
466 s = t0
467 else:
468 s = t1
469 dta[i] = s
470
471 return DatetimeIndex._simple_new(dta, name=self.name)
472
473 # --------------------------------------------------------------------
474 # Indexing Methods
475
476 def _parsed_string_to_bounds(self, reso: Resolution, parsed: dt.datetime):
477 """
478 Calculate datetime bounds for parsed time string and its resolution.
479
480 Parameters
481 ----------
482 reso : Resolution
483 Resolution provided by parsed string.
484 parsed : datetime
485 Datetime from parsed string.
486
487 Returns
488 -------
489 lower, upper: pd.Timestamp
490 """
491 per = Period(parsed, freq=reso.attr_abbrev)
492 start, end = per.start_time, per.end_time
493
494 # GH 24076
495 # If an incoming date string contained a UTC offset, need to localize
496 # the parsed date to this offset first before aligning with the index's
497 # timezone
498 start = start.tz_localize(parsed.tzinfo)
499 end = end.tz_localize(parsed.tzinfo)
500
501 if parsed.tzinfo is not None:
502 if self.tz is None:
503 raise ValueError(
504 "The index must be timezone aware when indexing "
505 "with a date string with a UTC offset"
506 )
507 # The flipped case with parsed.tz is None and self.tz is not None
508 # is ruled out bc parsed and reso are produced by _parse_with_reso,
509 # which localizes parsed.
510 return start, end
511
512 def _parse_with_reso(self, label: str):
513 parsed, reso = super()._parse_with_reso(label)
514
515 parsed = Timestamp(parsed)
516
517 if self.tz is not None and parsed.tzinfo is None:
518 # we special-case timezone-naive strings and timezone-aware
519 # DatetimeIndex
520 # https://github.com/pandas-dev/pandas/pull/36148#issuecomment-687883081
521 parsed = parsed.tz_localize(self.tz)
522
523 return parsed, reso
524
525 def _disallow_mismatched_indexing(self, key) -> None:
526 """
527 Check for mismatched-tzawareness indexing and re-raise as KeyError.
528 """
529 # we get here with isinstance(key, self._data._recognized_scalars)
530 try:
531 # GH#36148
532 self._data._assert_tzawareness_compat(key)
533 except TypeError as err:
534 raise KeyError(key) from err
535
536 def get_loc(self, key):
537 """
538 Get integer location for requested label
539
540 Returns
541 -------
542 loc : int
543 """
544 self._check_indexing_error(key)
545
546 orig_key = key
547 if is_valid_na_for_dtype(key, self.dtype):
548 key = NaT
549
550 if isinstance(key, self._data._recognized_scalars):
551 # needed to localize naive datetimes
552 self._disallow_mismatched_indexing(key)
553 key = Timestamp(key)
554
555 elif isinstance(key, str):
556 try:
557 parsed, reso = self._parse_with_reso(key)
558 except (ValueError, pytz.NonExistentTimeError) as err:
559 raise KeyError(key) from err
560 self._disallow_mismatched_indexing(parsed)
561
562 if self._can_partial_date_slice(reso):
563 try:
564 return self._partial_date_slice(reso, parsed)
565 except KeyError as err:
566 raise KeyError(key) from err
567
568 key = parsed
569
570 elif isinstance(key, dt.timedelta):
571 # GH#20464
572 raise TypeError(
573 f"Cannot index {type(self).__name__} with {type(key).__name__}"
574 )
575
576 elif isinstance(key, dt.time):
577 return self.indexer_at_time(key)
578
579 else:
580 # unrecognized type
581 raise KeyError(key)
582
583 try:
584 return Index.get_loc(self, key)
585 except KeyError as err:
586 raise KeyError(orig_key) from err
587
588 @doc(DatetimeTimedeltaMixin._maybe_cast_slice_bound)
589 def _maybe_cast_slice_bound(self, label, side: str):
590 # GH#42855 handle date here instead of get_slice_bound
591 if isinstance(label, dt.date) and not isinstance(label, dt.datetime):
592 # Pandas supports slicing with dates, treated as datetimes at midnight.
593 # https://github.com/pandas-dev/pandas/issues/31501
594 label = Timestamp(label).to_pydatetime()
595
596 label = super()._maybe_cast_slice_bound(label, side)
597 self._data._assert_tzawareness_compat(label)
598 return Timestamp(label)
599
600 def slice_indexer(self, start=None, end=None, step=None):
601 """
602 Return indexer for specified label slice.
603 Index.slice_indexer, customized to handle time slicing.
604
605 In addition to functionality provided by Index.slice_indexer, does the
606 following:
607
608 - if both `start` and `end` are instances of `datetime.time`, it
609 invokes `indexer_between_time`
610 - if `start` and `end` are both either string or None perform
611 value-based selection in non-monotonic cases.
612
613 """
614 # For historical reasons DatetimeIndex supports slices between two
615 # instances of datetime.time as if it were applying a slice mask to
616 # an array of (self.hour, self.minute, self.seconds, self.microsecond).
617 if isinstance(start, dt.time) and isinstance(end, dt.time):
618 if step is not None and step != 1:
619 raise ValueError("Must have step size of 1 with time slices")
620 return self.indexer_between_time(start, end)
621
622 if isinstance(start, dt.time) or isinstance(end, dt.time):
623 raise KeyError("Cannot mix time and non-time slice keys")
624
625 def check_str_or_none(point) -> bool:
626 return point is not None and not isinstance(point, str)
627
628 # GH#33146 if start and end are combinations of str and None and Index is not
629 # monotonic, we can not use Index.slice_indexer because it does not honor the
630 # actual elements, is only searching for start and end
631 if (
632 check_str_or_none(start)
633 or check_str_or_none(end)
634 or self.is_monotonic_increasing
635 ):
636 return Index.slice_indexer(self, start, end, step)
637
638 mask = np.array(True)
639 raise_mask = np.array(True)
640 if start is not None:
641 start_casted = self._maybe_cast_slice_bound(start, "left")
642 mask = start_casted <= self
643 raise_mask = start_casted == self
644
645 if end is not None:
646 end_casted = self._maybe_cast_slice_bound(end, "right")
647 mask = (self <= end_casted) & mask
648 raise_mask = (end_casted == self) | raise_mask
649
650 if not raise_mask.any():
651 raise KeyError(
652 "Value based partial slicing on non-monotonic DatetimeIndexes "
653 "with non-existing keys is not allowed.",
654 )
655 indexer = mask.nonzero()[0][::step]
656 if len(indexer) == len(self):
657 return slice(None)
658 else:
659 return indexer
660
661 # --------------------------------------------------------------------
662
663 @property
664 def inferred_type(self) -> str:
665 # b/c datetime is represented as microseconds since the epoch, make
666 # sure we can't have ambiguous indexing
667 return "datetime64"
668
669 def indexer_at_time(self, time, asof: bool = False) -> npt.NDArray[np.intp]:
670 """
671 Return index locations of values at particular time of day.
672
673 Parameters
674 ----------
675 time : datetime.time or str
676 Time passed in either as object (datetime.time) or as string in
677 appropriate format ("%H:%M", "%H%M", "%I:%M%p", "%I%M%p",
678 "%H:%M:%S", "%H%M%S", "%I:%M:%S%p", "%I%M%S%p").
679
680 Returns
681 -------
682 np.ndarray[np.intp]
683
684 See Also
685 --------
686 indexer_between_time : Get index locations of values between particular
687 times of day.
688 DataFrame.at_time : Select values at particular time of day.
689 """
690 if asof:
691 raise NotImplementedError("'asof' argument is not supported")
692
693 if isinstance(time, str):
694 from dateutil.parser import parse
695
696 time = parse(time).time()
697
698 if time.tzinfo:
699 if self.tz is None:
700 raise ValueError("Index must be timezone aware.")
701 time_micros = self.tz_convert(time.tzinfo)._get_time_micros()
702 else:
703 time_micros = self._get_time_micros()
704 micros = _time_to_micros(time)
705 return (time_micros == micros).nonzero()[0]
706
707 def indexer_between_time(
708 self, start_time, end_time, include_start: bool = True, include_end: bool = True
709 ) -> npt.NDArray[np.intp]:
710 """
711 Return index locations of values between particular times of day.
712
713 Parameters
714 ----------
715 start_time, end_time : datetime.time, str
716 Time passed either as object (datetime.time) or as string in
717 appropriate format ("%H:%M", "%H%M", "%I:%M%p", "%I%M%p",
718 "%H:%M:%S", "%H%M%S", "%I:%M:%S%p","%I%M%S%p").
719 include_start : bool, default True
720 include_end : bool, default True
721
722 Returns
723 -------
724 np.ndarray[np.intp]
725
726 See Also
727 --------
728 indexer_at_time : Get index locations of values at particular time of day.
729 DataFrame.between_time : Select values between particular times of day.
730 """
731 start_time = to_time(start_time)
732 end_time = to_time(end_time)
733 time_micros = self._get_time_micros()
734 start_micros = _time_to_micros(start_time)
735 end_micros = _time_to_micros(end_time)
736
737 if include_start and include_end:
738 lop = rop = operator.le
739 elif include_start:
740 lop = operator.le
741 rop = operator.lt
742 elif include_end:
743 lop = operator.lt
744 rop = operator.le
745 else:
746 lop = rop = operator.lt
747
748 if start_time <= end_time:
749 join_op = operator.and_
750 else:
751 join_op = operator.or_
752
753 mask = join_op(lop(start_micros, time_micros), rop(time_micros, end_micros))
754
755 return mask.nonzero()[0]
756
757
758def date_range(
759 start=None,
760 end=None,
761 periods=None,
762 freq=None,
763 tz=None,
764 normalize: bool = False,
765 name: Hashable = None,
766 inclusive: IntervalClosedType = "both",
767 *,
768 unit: str | None = None,
769 **kwargs,
770) -> DatetimeIndex:
771 """
772 Return a fixed frequency DatetimeIndex.
773
774 Returns the range of equally spaced time points (where the difference between any
775 two adjacent points is specified by the given frequency) such that they all
776 satisfy `start <[=] x <[=] end`, where the first one and the last one are, resp.,
777 the first and last time points in that range that fall on the boundary of ``freq``
778 (if given as a frequency string) or that are valid for ``freq`` (if given as a
779 :class:`pandas.tseries.offsets.DateOffset`). (If exactly one of ``start``,
780 ``end``, or ``freq`` is *not* specified, this missing parameter can be computed
781 given ``periods``, the number of timesteps in the range. See the note below.)
782
783 Parameters
784 ----------
785 start : str or datetime-like, optional
786 Left bound for generating dates.
787 end : str or datetime-like, optional
788 Right bound for generating dates.
789 periods : int, optional
790 Number of periods to generate.
791 freq : str, datetime.timedelta, or DateOffset, default 'D'
792 Frequency strings can have multiples, e.g. '5H'. See
793 :ref:`here <timeseries.offset_aliases>` for a list of
794 frequency aliases.
795 tz : str or tzinfo, optional
796 Time zone name for returning localized DatetimeIndex, for example
797 'Asia/Hong_Kong'. By default, the resulting DatetimeIndex is
798 timezone-naive unless timezone-aware datetime-likes are passed.
799 normalize : bool, default False
800 Normalize start/end dates to midnight before generating date range.
801 name : str, default None
802 Name of the resulting DatetimeIndex.
803 inclusive : {"both", "neither", "left", "right"}, default "both"
804 Include boundaries; Whether to set each bound as closed or open.
805
806 .. versionadded:: 1.4.0
807 unit : str, default None
808 Specify the desired resolution of the result.
809
810 .. versionadded:: 2.0.0
811 **kwargs
812 For compatibility. Has no effect on the result.
813
814 Returns
815 -------
816 DatetimeIndex
817
818 See Also
819 --------
820 DatetimeIndex : An immutable container for datetimes.
821 timedelta_range : Return a fixed frequency TimedeltaIndex.
822 period_range : Return a fixed frequency PeriodIndex.
823 interval_range : Return a fixed frequency IntervalIndex.
824
825 Notes
826 -----
827 Of the four parameters ``start``, ``end``, ``periods``, and ``freq``,
828 exactly three must be specified. If ``freq`` is omitted, the resulting
829 ``DatetimeIndex`` will have ``periods`` linearly spaced elements between
830 ``start`` and ``end`` (closed on both sides).
831
832 To learn more about the frequency strings, please see `this link
833 <https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases>`__.
834
835 Examples
836 --------
837 **Specifying the values**
838
839 The next four examples generate the same `DatetimeIndex`, but vary
840 the combination of `start`, `end` and `periods`.
841
842 Specify `start` and `end`, with the default daily frequency.
843
844 >>> pd.date_range(start='1/1/2018', end='1/08/2018')
845 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
846 '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08'],
847 dtype='datetime64[ns]', freq='D')
848
849 Specify timezone-aware `start` and `end`, with the default daily frequency.
850
851 >>> pd.date_range(
852 ... start=pd.to_datetime("1/1/2018").tz_localize("Europe/Berlin"),
853 ... end=pd.to_datetime("1/08/2018").tz_localize("Europe/Berlin"),
854 ... )
855 DatetimeIndex(['2018-01-01 00:00:00+01:00', '2018-01-02 00:00:00+01:00',
856 '2018-01-03 00:00:00+01:00', '2018-01-04 00:00:00+01:00',
857 '2018-01-05 00:00:00+01:00', '2018-01-06 00:00:00+01:00',
858 '2018-01-07 00:00:00+01:00', '2018-01-08 00:00:00+01:00'],
859 dtype='datetime64[ns, Europe/Berlin]', freq='D')
860
861 Specify `start` and `periods`, the number of periods (days).
862
863 >>> pd.date_range(start='1/1/2018', periods=8)
864 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
865 '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08'],
866 dtype='datetime64[ns]', freq='D')
867
868 Specify `end` and `periods`, the number of periods (days).
869
870 >>> pd.date_range(end='1/1/2018', periods=8)
871 DatetimeIndex(['2017-12-25', '2017-12-26', '2017-12-27', '2017-12-28',
872 '2017-12-29', '2017-12-30', '2017-12-31', '2018-01-01'],
873 dtype='datetime64[ns]', freq='D')
874
875 Specify `start`, `end`, and `periods`; the frequency is generated
876 automatically (linearly spaced).
877
878 >>> pd.date_range(start='2018-04-24', end='2018-04-27', periods=3)
879 DatetimeIndex(['2018-04-24 00:00:00', '2018-04-25 12:00:00',
880 '2018-04-27 00:00:00'],
881 dtype='datetime64[ns]', freq=None)
882
883 **Other Parameters**
884
885 Changed the `freq` (frequency) to ``'M'`` (month end frequency).
886
887 >>> pd.date_range(start='1/1/2018', periods=5, freq='M')
888 DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31', '2018-04-30',
889 '2018-05-31'],
890 dtype='datetime64[ns]', freq='M')
891
892 Multiples are allowed
893
894 >>> pd.date_range(start='1/1/2018', periods=5, freq='3M')
895 DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31',
896 '2019-01-31'],
897 dtype='datetime64[ns]', freq='3M')
898
899 `freq` can also be specified as an Offset object.
900
901 >>> pd.date_range(start='1/1/2018', periods=5, freq=pd.offsets.MonthEnd(3))
902 DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31',
903 '2019-01-31'],
904 dtype='datetime64[ns]', freq='3M')
905
906 Specify `tz` to set the timezone.
907
908 >>> pd.date_range(start='1/1/2018', periods=5, tz='Asia/Tokyo')
909 DatetimeIndex(['2018-01-01 00:00:00+09:00', '2018-01-02 00:00:00+09:00',
910 '2018-01-03 00:00:00+09:00', '2018-01-04 00:00:00+09:00',
911 '2018-01-05 00:00:00+09:00'],
912 dtype='datetime64[ns, Asia/Tokyo]', freq='D')
913
914 `inclusive` controls whether to include `start` and `end` that are on the
915 boundary. The default, "both", includes boundary points on either end.
916
917 >>> pd.date_range(start='2017-01-01', end='2017-01-04', inclusive="both")
918 DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03', '2017-01-04'],
919 dtype='datetime64[ns]', freq='D')
920
921 Use ``inclusive='left'`` to exclude `end` if it falls on the boundary.
922
923 >>> pd.date_range(start='2017-01-01', end='2017-01-04', inclusive='left')
924 DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'],
925 dtype='datetime64[ns]', freq='D')
926
927 Use ``inclusive='right'`` to exclude `start` if it falls on the boundary, and
928 similarly ``inclusive='neither'`` will exclude both `start` and `end`.
929
930 >>> pd.date_range(start='2017-01-01', end='2017-01-04', inclusive='right')
931 DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'],
932 dtype='datetime64[ns]', freq='D')
933
934 **Specify a unit**
935
936 >>> pd.date_range(start="2017-01-01", periods=10, freq="100AS", unit="s")
937 DatetimeIndex(['2017-01-01', '2117-01-01', '2217-01-01', '2317-01-01',
938 '2417-01-01', '2517-01-01', '2617-01-01', '2717-01-01',
939 '2817-01-01', '2917-01-01'],
940 dtype='datetime64[s]', freq='100AS-JAN')
941 """
942 if freq is None and com.any_none(periods, start, end):
943 freq = "D"
944
945 dtarr = DatetimeArray._generate_range(
946 start=start,
947 end=end,
948 periods=periods,
949 freq=freq,
950 tz=tz,
951 normalize=normalize,
952 inclusive=inclusive,
953 unit=unit,
954 **kwargs,
955 )
956 return DatetimeIndex._simple_new(dtarr, name=name)
957
958
959def bdate_range(
960 start=None,
961 end=None,
962 periods: int | None = None,
963 freq: Frequency = "B",
964 tz=None,
965 normalize: bool = True,
966 name: Hashable = None,
967 weekmask=None,
968 holidays=None,
969 inclusive: IntervalClosedType = "both",
970 **kwargs,
971) -> DatetimeIndex:
972 """
973 Return a fixed frequency DatetimeIndex with business day as the default.
974
975 Parameters
976 ----------
977 start : str or datetime-like, default None
978 Left bound for generating dates.
979 end : str or datetime-like, default None
980 Right bound for generating dates.
981 periods : int, default None
982 Number of periods to generate.
983 freq : str, Timedelta, datetime.timedelta, or DateOffset, default 'B'
984 Frequency strings can have multiples, e.g. '5H'. The default is
985 business daily ('B').
986 tz : str or None
987 Time zone name for returning localized DatetimeIndex, for example
988 Asia/Beijing.
989 normalize : bool, default False
990 Normalize start/end dates to midnight before generating date range.
991 name : str, default None
992 Name of the resulting DatetimeIndex.
993 weekmask : str or None, default None
994 Weekmask of valid business days, passed to ``numpy.busdaycalendar``,
995 only used when custom frequency strings are passed. The default
996 value None is equivalent to 'Mon Tue Wed Thu Fri'.
997 holidays : list-like or None, default None
998 Dates to exclude from the set of valid business days, passed to
999 ``numpy.busdaycalendar``, only used when custom frequency strings
1000 are passed.
1001 inclusive : {"both", "neither", "left", "right"}, default "both"
1002 Include boundaries; Whether to set each bound as closed or open.
1003
1004 .. versionadded:: 1.4.0
1005 **kwargs
1006 For compatibility. Has no effect on the result.
1007
1008 Returns
1009 -------
1010 DatetimeIndex
1011
1012 Notes
1013 -----
1014 Of the four parameters: ``start``, ``end``, ``periods``, and ``freq``,
1015 exactly three must be specified. Specifying ``freq`` is a requirement
1016 for ``bdate_range``. Use ``date_range`` if specifying ``freq`` is not
1017 desired.
1018
1019 To learn more about the frequency strings, please see `this link
1020 <https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases>`__.
1021
1022 Examples
1023 --------
1024 Note how the two weekend days are skipped in the result.
1025
1026 >>> pd.bdate_range(start='1/1/2018', end='1/08/2018')
1027 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
1028 '2018-01-05', '2018-01-08'],
1029 dtype='datetime64[ns]', freq='B')
1030 """
1031 if freq is None:
1032 msg = "freq must be specified for bdate_range; use date_range instead"
1033 raise TypeError(msg)
1034
1035 if isinstance(freq, str) and freq.startswith("C"):
1036 try:
1037 weekmask = weekmask or "Mon Tue Wed Thu Fri"
1038 freq = prefix_mapping[freq](holidays=holidays, weekmask=weekmask)
1039 except (KeyError, TypeError) as err:
1040 msg = f"invalid custom frequency string: {freq}"
1041 raise ValueError(msg) from err
1042 elif holidays or weekmask:
1043 msg = (
1044 "a custom frequency string is required when holidays or "
1045 f"weekmask are passed, got frequency {freq}"
1046 )
1047 raise ValueError(msg)
1048
1049 return date_range(
1050 start=start,
1051 end=end,
1052 periods=periods,
1053 freq=freq,
1054 tz=tz,
1055 normalize=normalize,
1056 name=name,
1057 inclusive=inclusive,
1058 **kwargs,
1059 )
1060
1061
1062def _time_to_micros(time_obj: dt.time) -> int:
1063 seconds = time_obj.hour * 60 * 60 + 60 * time_obj.minute + time_obj.second
1064 return 1_000_000 * seconds + time_obj.microsecond