1from __future__ import annotations
2
3import datetime as dt
4import operator
5from typing import (
6 TYPE_CHECKING,
7 Self,
8)
9import warnings
10
11import numpy as np
12
13from pandas._libs import (
14 NaT,
15 Period,
16 Timestamp,
17 index as libindex,
18 lib,
19)
20from pandas._libs.tslibs import (
21 Resolution,
22 Tick,
23 Timedelta,
24 periods_per_day,
25 timezones,
26 to_offset,
27)
28from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
29from pandas._libs.tslibs.offsets import (
30 DateOffset,
31 prefix_mapping,
32)
33from pandas.errors import Pandas4Warning
34from pandas.util._decorators import (
35 cache_readonly,
36 set_module,
37)
38from pandas.util._exceptions import find_stack_level
39
40from pandas.core.dtypes.common import is_scalar
41from pandas.core.dtypes.dtypes import (
42 ArrowDtype,
43 DatetimeTZDtype,
44)
45from pandas.core.dtypes.generic import ABCSeries
46from pandas.core.dtypes.missing import is_valid_na_for_dtype
47
48from pandas.core.arrays.datetimes import (
49 DatetimeArray,
50 tz_to_dtype,
51)
52import pandas.core.common as com
53from pandas.core.indexes.base import (
54 Index,
55 maybe_extract_name,
56)
57from pandas.core.indexes.datetimelike import DatetimeTimedeltaMixin
58from pandas.core.indexes.extension import inherit_names
59from pandas.core.tools.times import to_time
60
61if TYPE_CHECKING:
62 from collections.abc import Hashable
63
64 from pandas._typing import (
65 Dtype,
66 DtypeObj,
67 Frequency,
68 IntervalClosedType,
69 TimeAmbiguous,
70 TimeNonexistent,
71 npt,
72 TimeUnit,
73 )
74
75 from pandas.core.api import (
76 DataFrame,
77 PeriodIndex,
78 )
79
80from pandas._libs.tslibs.dtypes import OFFSET_TO_PERIOD_FREQSTR
81
82
83def _new_DatetimeIndex(cls, d):
84 """
85 This is called upon unpickling, rather than the default which doesn't
86 have arguments and breaks __new__
87 """
88 if "data" in d and not isinstance(d["data"], DatetimeIndex):
89 # Avoid need to verify integrity by calling simple_new directly
90 data = d.pop("data")
91 if not isinstance(data, DatetimeArray):
92 # For backward compat with older pickles, we may need to construct
93 # a DatetimeArray to adapt to the newer _simple_new signature
94 tz = d.pop("tz")
95 freq = d.pop("freq")
96 dta = DatetimeArray._simple_new(data, dtype=tz_to_dtype(tz), freq=freq)
97 else:
98 dta = data
99 for key in ["tz", "freq"]:
100 # These are already stored in our DatetimeArray; if they are
101 # also in the pickle and don't match, we have a problem.
102 if key in d:
103 assert d[key] == getattr(dta, key)
104 d.pop(key)
105 result = cls._simple_new(dta, **d)
106 else:
107 with warnings.catch_warnings():
108 # TODO: If we knew what was going in to **d, we might be able to
109 # go through _simple_new instead
110 warnings.simplefilter("ignore")
111 result = cls.__new__(cls, **d)
112
113 return result
114
115
116@inherit_names(
117 DatetimeArray._field_ops
118 + [
119 method
120 for method in DatetimeArray._datetimelike_methods
121 if method not in ("tz_localize", "tz_convert", "strftime")
122 ],
123 DatetimeArray,
124 wrap=True,
125)
126@inherit_names(["is_normalized"], DatetimeArray, cache=True)
127@inherit_names(
128 [
129 "tz",
130 "tzinfo",
131 "dtype",
132 "to_pydatetime",
133 "date",
134 "time",
135 "timetz",
136 "std",
137 *DatetimeArray._bool_ops,
138 ],
139 DatetimeArray,
140)
141@set_module("pandas")
142class DatetimeIndex(DatetimeTimedeltaMixin):
143 """
144 Immutable ndarray-like of datetime64 data.
145
146 Represented internally as int64, and which can be boxed to Timestamp objects
147 that are subclasses of datetime and carry metadata.
148
149 .. versionchanged:: 2.0.0
150 The various numeric date/time attributes (:attr:`~DatetimeIndex.day`,
151 :attr:`~DatetimeIndex.month`, :attr:`~DatetimeIndex.year` etc.) now have dtype
152 ``int32``. Previously they had dtype ``int64``.
153
154 Parameters
155 ----------
156 data : array-like (1-dimensional)
157 Datetime-like data to construct index with.
158 freq : str or pandas offset object, optional
159 One of pandas date offset strings or corresponding objects. The string
160 'infer' can be passed in order to set the frequency of the index as the
161 inferred frequency upon creation.
162 tz : zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or str
163 Set the Timezone of the data.
164 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
165 When clocks moved backward due to DST, ambiguous times may arise.
166 For example in Central European Time (UTC+01), when going from 03:00
167 DST to 02:00 non-DST, 02:30:00 local time occurs both at 00:30:00 UTC
168 and at 01:30:00 UTC. In such a situation, the `ambiguous` parameter
169 dictates how ambiguous times should be handled.
170
171 - 'infer' will attempt to infer fall dst-transition hours based on
172 order
173 - bool-ndarray where True signifies a DST time, False signifies a
174 non-DST time (note that this flag is only applicable for ambiguous
175 times)
176 - 'NaT' will return NaT where there are ambiguous times
177 - 'raise' will raise a ValueError if there are ambiguous times.
178 dayfirst : bool, default False
179 If True, parse dates in `data` with the day first order.
180 yearfirst : bool, default False
181 If True parse dates in `data` with the year first order.
182 dtype : numpy.dtype or DatetimeTZDtype or str, default None
183 Note that the only NumPy dtype allowed is `datetime64[ns]`.
184 copy : bool, default None
185 Whether to copy input data, only relevant for array, Series, and Index
186 inputs (for other input, e.g. a list, a new array is created anyway).
187 Defaults to True for array input and False for Index/Series.
188 Set to False to avoid copying array input at your own risk (if you
189 know the input data won't be modified elsewhere).
190 Set to True to force copying Series/Index up front.
191 name : label, default None
192 Name to be stored in the index.
193
194 Attributes
195 ----------
196 year
197 month
198 day
199 hour
200 minute
201 second
202 microsecond
203 nanosecond
204 date
205 time
206 timetz
207 dayofyear
208 day_of_year
209 dayofweek
210 day_of_week
211 weekday
212 quarter
213 tz
214 freq
215 freqstr
216 is_month_start
217 is_month_end
218 is_quarter_start
219 is_quarter_end
220 is_year_start
221 is_year_end
222 is_leap_year
223 inferred_freq
224
225 Methods
226 -------
227 normalize
228 strftime
229 snap
230 tz_convert
231 tz_localize
232 round
233 floor
234 ceil
235 to_period
236 to_pydatetime
237 to_series
238 to_frame
239 to_julian_date
240 month_name
241 day_name
242 mean
243 std
244
245 See Also
246 --------
247 Index : The base pandas Index type.
248 TimedeltaIndex : Index of timedelta64 data.
249 PeriodIndex : Index of Period data.
250 to_datetime : Convert argument to datetime.
251 date_range : Create a fixed-frequency DatetimeIndex.
252
253 Notes
254 -----
255 To learn more about the frequency strings, please see
256 :ref:`this link<timeseries.offset_aliases>`.
257
258 Examples
259 --------
260 >>> idx = pd.DatetimeIndex(["1/1/2020 10:00:00+00:00", "2/1/2020 11:00:00+00:00"])
261 >>> idx
262 DatetimeIndex(['2020-01-01 10:00:00+00:00', '2020-02-01 11:00:00+00:00'],
263 dtype='datetime64[us, UTC]', freq=None)
264 """
265
266 _typ = "datetimeindex"
267
268 _data_cls = DatetimeArray
269 _supports_partial_string_indexing = True
270
271 @property
272 def _engine_type(self) -> type[libindex.DatetimeEngine]:
273 return libindex.DatetimeEngine
274
275 _data: DatetimeArray
276 _values: DatetimeArray
277 tz: dt.tzinfo | None
278
279 # --------------------------------------------------------------------
280 # methods that dispatch to DatetimeArray and wrap result
281
282 def strftime(self, date_format) -> Index:
283 """
284 Convert to Index using specified date_format.
285
286 Return an Index of formatted strings specified by date_format, which
287 supports the same string format as the python standard library. Details
288 of the string format can be found in `python string format
289 doc <https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior>`__.
290
291 Formats supported by the C `strftime` API but not by the python string format
292 doc (such as `"%R"`, `"%r"`) are not officially supported and should be
293 preferably replaced with their supported equivalents (such as `"%H:%M"`,
294 `"%I:%M:%S %p"`).
295 Note that `PeriodIndex` support additional directives, detailed in
296 `Period.strftime`.
297
298 Parameters
299 ----------
300 date_format : str
301 Date format string (e.g. "%Y-%m-%d").
302
303 Returns
304 -------
305 ndarray[object]
306 NumPy ndarray of formatted strings.
307
308 See Also
309 --------
310 to_datetime : Convert the given argument to datetime.
311 DatetimeIndex.normalize : Return DatetimeIndex with times to midnight.
312 DatetimeIndex.round : Round the DatetimeIndex to the specified freq.
313 DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq.
314 Timestamp.strftime : Format a single Timestamp.
315 Period.strftime : Format a single Period.
316
317 Examples
318 --------
319 >>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"), periods=3, freq="s")
320 >>> rng.strftime("%B %d, %Y, %r")
321 Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM',
322 'March 10, 2018, 09:00:02 AM'],
323 dtype='str')
324 """
325 arr = self._data.strftime(date_format)
326 return Index(arr, name=self.name, dtype=arr.dtype, copy=False)
327
328 def tz_convert(self, tz) -> Self:
329 """
330 Convert tz-aware Datetime Array/Index from one time zone to another.
331
332 Parameters
333 ----------
334 tz : str, zoneinfo.ZoneInfo, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None
335 Time zone for time. Corresponding timestamps would be converted
336 to this time zone of the Datetime Array/Index. A `tz` of None will
337 convert to UTC and remove the timezone information.
338
339 Returns
340 -------
341 Array or Index
342 Datetme Array/Index with target `tz`.
343
344 Raises
345 ------
346 TypeError
347 If Datetime Array/Index is tz-naive.
348
349 See Also
350 --------
351 DatetimeIndex.tz : A timezone that has a variable offset from UTC.
352 DatetimeIndex.tz_localize : Localize tz-naive DatetimeIndex to a
353 given time zone, or remove timezone from a tz-aware DatetimeIndex.
354
355 Examples
356 --------
357 With the `tz` parameter, we can change the DatetimeIndex
358 to other time zones:
359
360 >>> dti = pd.date_range(
361 ... start="2014-08-01 09:00", freq="h", periods=3, tz="Europe/Berlin"
362 ... )
363
364 >>> dti
365 DatetimeIndex(['2014-08-01 09:00:00+02:00',
366 '2014-08-01 10:00:00+02:00',
367 '2014-08-01 11:00:00+02:00'],
368 dtype='datetime64[us, Europe/Berlin]', freq='h')
369
370 >>> dti.tz_convert("US/Central")
371 DatetimeIndex(['2014-08-01 02:00:00-05:00',
372 '2014-08-01 03:00:00-05:00',
373 '2014-08-01 04:00:00-05:00'],
374 dtype='datetime64[us, US/Central]', freq='h')
375
376 With the ``tz=None``, we can remove the timezone (after converting
377 to UTC if necessary):
378
379 >>> dti = pd.date_range(
380 ... start="2014-08-01 09:00", freq="h", periods=3, tz="Europe/Berlin"
381 ... )
382
383 >>> dti
384 DatetimeIndex(['2014-08-01 09:00:00+02:00',
385 '2014-08-01 10:00:00+02:00',
386 '2014-08-01 11:00:00+02:00'],
387 dtype='datetime64[us, Europe/Berlin]', freq='h')
388
389 >>> dti.tz_convert(None)
390 DatetimeIndex(['2014-08-01 07:00:00',
391 '2014-08-01 08:00:00',
392 '2014-08-01 09:00:00'],
393 dtype='datetime64[us]', freq='h')
394 """ # noqa: E501
395 arr = self._data.tz_convert(tz)
396 return type(self)._simple_new(arr, name=self.name, refs=self._references)
397
398 def tz_localize(
399 self,
400 tz,
401 ambiguous: TimeAmbiguous = "raise",
402 nonexistent: TimeNonexistent = "raise",
403 ) -> Self:
404 """
405 Localize tz-naive Datetime Array/Index to tz-aware Datetime Array/Index.
406
407 This method takes a time zone (tz) naive Datetime Array/Index object
408 and makes this time zone aware. It does not move the time to another
409 time zone.
410
411 This method can also be used to do the inverse -- to create a time
412 zone unaware object from an aware object. To that end, pass `tz=None`.
413
414 Parameters
415 ----------
416 tz : str, zoneinfo.ZoneInfo,, pytz.timezone, dateutil.tz.tzfile, datetime.tzinfo or None
417 Time zone to convert timestamps to. Passing ``None`` will
418 remove the time zone information preserving local time.
419 ambiguous : 'infer', 'NaT', bool array, default 'raise'
420 When clocks moved backward due to DST, ambiguous times may arise.
421 For example in Central European Time (UTC+01), when going from
422 03:00 DST to 02:00 non-DST, 02:30:00 local time occurs both at
423 00:30:00 UTC and at 01:30:00 UTC. In such a situation, the
424 `ambiguous` parameter dictates how ambiguous times should be
425 handled.
426
427 - 'infer' will attempt to infer fall dst-transition hours based on
428 order
429 - bool-ndarray where True signifies a DST time, False signifies a
430 non-DST time (note that this flag is only applicable for
431 ambiguous times)
432 - 'NaT' will return NaT where there are ambiguous times
433 - 'raise' will raise a ValueError if there are ambiguous
434 times.
435
436 nonexistent : 'shift_forward', 'shift_backward, 'NaT', timedelta, \
437 default 'raise'
438 A nonexistent time does not exist in a particular timezone
439 where clocks moved forward due to DST.
440
441 - 'shift_forward' will shift the nonexistent time forward to the
442 closest existing time
443 - 'shift_backward' will shift the nonexistent time backward to the
444 closest existing time
445 - 'NaT' will return NaT where there are nonexistent times
446 - timedelta objects will shift nonexistent times by the timedelta
447 - 'raise' will raise a ValueError if there are
448 nonexistent times.
449
450 Returns
451 -------
452 Same type as self
453 Array/Index converted to the specified time zone.
454
455 Raises
456 ------
457 TypeError
458 If the Datetime Array/Index is tz-aware and tz is not None.
459
460 See Also
461 --------
462 DatetimeIndex.tz_convert : Convert tz-aware DatetimeIndex from
463 one time zone to another.
464
465 Examples
466 --------
467 >>> tz_naive = pd.date_range('2018-03-01 09:00', periods=3)
468 >>> tz_naive
469 DatetimeIndex(['2018-03-01 09:00:00', '2018-03-02 09:00:00',
470 '2018-03-03 09:00:00'],
471 dtype='datetime64[us]', freq='D')
472
473 Localize DatetimeIndex in US/Eastern time zone:
474
475 >>> tz_aware = tz_naive.tz_localize(tz='US/Eastern')
476 >>> tz_aware
477 DatetimeIndex(['2018-03-01 09:00:00-05:00',
478 '2018-03-02 09:00:00-05:00',
479 '2018-03-03 09:00:00-05:00'],
480 dtype='datetime64[us, US/Eastern]', freq=None)
481
482 With the ``tz=None``, we can remove the time zone information
483 while keeping the local time (not converted to UTC):
484
485 >>> tz_aware.tz_localize(None)
486 DatetimeIndex(['2018-03-01 09:00:00', '2018-03-02 09:00:00',
487 '2018-03-03 09:00:00'],
488 dtype='datetime64[us]', freq=None)
489
490 Be careful with DST changes. When there is sequential data, pandas can
491 infer the DST time:
492
493 >>> s = pd.to_datetime(pd.Series(['2018-10-28 01:30:00',
494 ... '2018-10-28 02:00:00',
495 ... '2018-10-28 02:30:00',
496 ... '2018-10-28 02:00:00',
497 ... '2018-10-28 02:30:00',
498 ... '2018-10-28 03:00:00',
499 ... '2018-10-28 03:30:00']))
500 >>> s.dt.tz_localize('CET', ambiguous='infer')
501 0 2018-10-28 01:30:00+02:00
502 1 2018-10-28 02:00:00+02:00
503 2 2018-10-28 02:30:00+02:00
504 3 2018-10-28 02:00:00+01:00
505 4 2018-10-28 02:30:00+01:00
506 5 2018-10-28 03:00:00+01:00
507 6 2018-10-28 03:30:00+01:00
508 dtype: datetime64[us, CET]
509
510 In some cases, inferring the DST is impossible. In such cases, you can
511 pass an ndarray to the ambiguous parameter to set the DST explicitly
512
513 >>> s = pd.to_datetime(pd.Series(['2018-10-28 01:20:00',
514 ... '2018-10-28 02:36:00',
515 ... '2018-10-28 03:46:00']))
516 >>> s.dt.tz_localize('CET', ambiguous=np.array([True, True, False]))
517 0 2018-10-28 01:20:00+02:00
518 1 2018-10-28 02:36:00+02:00
519 2 2018-10-28 03:46:00+01:00
520 dtype: datetime64[us, CET]
521
522 If the DST transition causes nonexistent times, you can shift these
523 dates forward or backwards with a timedelta object or `'shift_forward'`
524 or `'shift_backwards'`.
525
526 >>> s = pd.to_datetime(pd.Series(['2015-03-29 02:30:00',
527 ... '2015-03-29 03:30:00'], dtype="M8[ns]"))
528 >>> s.dt.tz_localize('Europe/Warsaw', nonexistent='shift_forward')
529 0 2015-03-29 03:00:00+02:00
530 1 2015-03-29 03:30:00+02:00
531 dtype: datetime64[ns, Europe/Warsaw]
532
533 >>> s.dt.tz_localize('Europe/Warsaw', nonexistent='shift_backward')
534 0 2015-03-29 01:59:59.999999999+01:00
535 1 2015-03-29 03:30:00+02:00
536 dtype: datetime64[ns, Europe/Warsaw]
537
538 >>> s.dt.tz_localize('Europe/Warsaw', nonexistent=pd.Timedelta('1h'))
539 0 2015-03-29 03:30:00+02:00
540 1 2015-03-29 03:30:00+02:00
541 dtype: datetime64[ns, Europe/Warsaw]
542 """ # noqa: E501
543 arr = self._data.tz_localize(tz, ambiguous, nonexistent)
544 return type(self)._simple_new(arr, name=self.name)
545
546 def to_period(self, freq=None) -> PeriodIndex:
547 """
548 Cast to PeriodArray/PeriodIndex at a particular frequency.
549
550 Converts DatetimeArray/Index to PeriodArray/PeriodIndex.
551
552 Parameters
553 ----------
554 freq : str or Period, optional
555 One of pandas' :ref:`period aliases <timeseries.period_aliases>`
556 or a Period object. Will be inferred by default.
557
558 Returns
559 -------
560 PeriodArray/PeriodIndex
561 Immutable ndarray holding ordinal values at a particular frequency.
562
563 Raises
564 ------
565 ValueError
566 When converting a DatetimeArray/Index with non-regular values,
567 so that a frequency cannot be inferred.
568
569 See Also
570 --------
571 PeriodIndex: Immutable ndarray holding ordinal values.
572 DatetimeIndex.to_pydatetime: Return DatetimeIndex as object.
573
574 Examples
575 --------
576 >>> df = pd.DataFrame(
577 ... {"y": [1, 2, 3]},
578 ... index=pd.to_datetime(
579 ... [
580 ... "2000-03-31 00:00:00",
581 ... "2000-05-31 00:00:00",
582 ... "2000-08-31 00:00:00",
583 ... ]
584 ... ),
585 ... )
586 >>> df.index.to_period("M")
587 PeriodIndex(['2000-03', '2000-05', '2000-08'],
588 dtype='period[M]')
589
590 Infer the daily frequency
591
592 >>> idx = pd.date_range("2017-01-01", periods=2)
593 >>> idx.to_period()
594 PeriodIndex(['2017-01-01', '2017-01-02'],
595 dtype='period[D]')
596 """
597 from pandas.core.indexes.api import PeriodIndex
598
599 arr = self._data.to_period(freq)
600 return PeriodIndex._simple_new(arr, name=self.name)
601
602 def to_julian_date(self) -> Index:
603 """
604 Convert TimeStamp to a Julian Date.
605
606 This method returns the number of days as a float since noon January 1, 4713 BC.
607
608 https://en.wikipedia.org/wiki/Julian_day
609
610 Returns
611 -------
612 ndarray or Index
613 Float values that represent each date in Julian Calendar.
614
615 See Also
616 --------
617 Timestamp.to_julian_date : Equivalent method on ``Timestamp`` objects.
618
619 Examples
620 --------
621 >>> idx = pd.DatetimeIndex(["2028-08-12 00:54", "2028-08-12 02:06"])
622 >>> idx.to_julian_date()
623 Index([2461995.5375, 2461995.5875], dtype='float64')
624 """
625 arr = self._data.to_julian_date()
626 return Index._simple_new(arr, name=self.name)
627
628 def isocalendar(self) -> DataFrame:
629 """
630 Calculate year, week, and day according to the ISO 8601 standard.
631 Returns
632 -------
633 DataFrame
634 With columns year, week and day.
635 See Also
636 --------
637 Timestamp.isocalendar : Function return a 3-tuple containing ISO year,
638 week number, and weekday for the given Timestamp object.
639 datetime.date.isocalendar : Return a named tuple object with
640 three components: year, week and weekday.
641
642 Examples
643 --------
644 >>> idx = pd.date_range(start="2019-12-29", freq="D", periods=4)
645 >>> idx.isocalendar()
646 year week day
647 2019-12-29 2019 52 7
648 2019-12-30 2020 1 1
649 2019-12-31 2020 1 2
650 2020-01-01 2020 1 3
651 >>> idx.isocalendar().week
652 2019-12-29 52
653 2019-12-30 1
654 2019-12-31 1
655 2020-01-01 1
656 Freq: D, Name: week, dtype: UInt32
657 """
658 df = self._data.isocalendar()
659 return df.set_index(self)
660
661 @cache_readonly
662 def _resolution_obj(self) -> Resolution:
663 return self._data._resolution_obj
664
665 # --------------------------------------------------------------------
666 # Constructors
667
668 def __new__(
669 cls,
670 data=None,
671 freq: Frequency | lib.NoDefault = lib.no_default,
672 tz=lib.no_default,
673 ambiguous: TimeAmbiguous = "raise",
674 dayfirst: bool = False,
675 yearfirst: bool = False,
676 dtype: Dtype | None = None,
677 copy: bool | None = None,
678 name: Hashable | None = None,
679 ) -> Self:
680 if is_scalar(data):
681 cls._raise_scalar_data_error(data)
682
683 # - Cases checked above all return/raise before reaching here - #
684
685 name = maybe_extract_name(name, data, cls)
686
687 # GH#63388
688 data, copy = cls._maybe_copy_array_input(data, copy, dtype)
689
690 if (
691 isinstance(data, DatetimeArray)
692 and freq is lib.no_default
693 and tz is lib.no_default
694 and dtype is None
695 ):
696 # fastpath, similar logic in TimedeltaIndex.__new__;
697 # Note in this particular case we retain non-nano.
698 if copy:
699 data = data.copy()
700 return cls._simple_new(data, name=name)
701
702 dtarr = DatetimeArray._from_sequence_not_strict(
703 data,
704 dtype=dtype,
705 copy=copy,
706 tz=tz,
707 freq=freq,
708 dayfirst=dayfirst,
709 yearfirst=yearfirst,
710 ambiguous=ambiguous,
711 )
712 refs = None
713 if not copy and isinstance(data, (Index, ABCSeries)):
714 refs = data._references
715
716 subarr = cls._simple_new(dtarr, name=name, refs=refs)
717 return subarr
718
719 # --------------------------------------------------------------------
720
721 @cache_readonly
722 def _is_dates_only(self) -> bool:
723 """
724 Return a boolean if we are only dates (and don't have a timezone)
725
726 Returns
727 -------
728 bool
729 """
730 if isinstance(self.freq, Tick):
731 delta = Timedelta(self.freq)
732
733 if delta % dt.timedelta(days=1) != dt.timedelta(days=0):
734 return False
735
736 return self._values._is_dates_only
737
738 def __reduce__(self):
739 d = {"data": self._data, "name": self.name}
740 return _new_DatetimeIndex, (type(self), d), None
741
742 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
743 """
744 Can we compare values of the given dtype to our own?
745 """
746 if isinstance(dtype, ArrowDtype):
747 # GH#62277
748 if dtype.kind != "M":
749 return False
750
751 pa_dtype = dtype.pyarrow_dtype
752 if (pa_dtype.tz is None) ^ (self.tz is None):
753 return False
754 return True
755
756 if self.tz is not None:
757 # If we have tz, we can compare to tzaware
758 return isinstance(dtype, DatetimeTZDtype)
759 # if we dont have tz, we can only compare to tznaive
760 return lib.is_np_dtype(dtype, "M")
761
762 # --------------------------------------------------------------------
763 # Rendering Methods
764
765 @cache_readonly
766 def _formatter_func(self):
767 # Note this is equivalent to the DatetimeIndexOpsMixin method but
768 # uses the maybe-cached self._is_dates_only instead of re-computing it.
769 from pandas.io.formats.format import get_format_datetime64
770
771 formatter = get_format_datetime64(is_dates_only=self._is_dates_only)
772 return lambda x: f"'{formatter(x)}'"
773
774 # --------------------------------------------------------------------
775 # Set Operation Methods
776
777 def _can_range_setop(self, other) -> bool:
778 # GH 46702: If self or other have non-UTC tzs, DST transitions prevent
779 # range representation due to no singular step
780 if (
781 self.tz is not None
782 and not timezones.is_utc(self.tz)
783 and not timezones.is_fixed_offset(self.tz)
784 ):
785 return False
786 if (
787 other.tz is not None
788 and not timezones.is_utc(other.tz)
789 and not timezones.is_fixed_offset(other.tz)
790 ):
791 return False
792 return super()._can_range_setop(other)
793
794 # --------------------------------------------------------------------
795
796 def _get_time_micros(self) -> npt.NDArray[np.int64]:
797 """
798 Return the number of microseconds since midnight.
799
800 Returns
801 -------
802 ndarray[int64_t]
803 """
804 values = self._data._local_timestamps()
805
806 ppd = periods_per_day(self._data._creso)
807
808 frac = values % ppd
809 if self.unit == "ns":
810 micros = frac // 1000
811 elif self.unit == "us":
812 micros = frac
813 elif self.unit == "ms":
814 micros = frac * 1000
815 elif self.unit == "s":
816 micros = frac * 1_000_000
817 else: # pragma: no cover
818 raise NotImplementedError(self.unit)
819
820 micros[self._isnan] = -1
821 return micros
822
823 def snap(self, freq: Frequency = "S") -> DatetimeIndex:
824 """
825 Snap time stamps to nearest occurring frequency.
826
827 Parameters
828 ----------
829 freq : str, Timedelta, datetime.timedelta, or DateOffset, default 'S'
830 Frequency strings can have multiples, e.g. '5h'. See
831 :ref:`here <timeseries.offset_aliases>` for a list of
832 frequency aliases.
833
834 Returns
835 -------
836 DatetimeIndex
837 Time stamps to nearest occurring `freq`.
838
839 See Also
840 --------
841 DatetimeIndex.round : Perform round operation on the data to the
842 specified `freq`.
843 DatetimeIndex.floor : Perform floor operation on the data to the
844 specified `freq`.
845
846 Examples
847 --------
848 >>> idx = pd.DatetimeIndex(
849 ... ["2023-01-01", "2023-01-02", "2023-02-01", "2023-02-02"],
850 ... dtype="M8[ns]",
851 ... )
852 >>> idx
853 DatetimeIndex(['2023-01-01', '2023-01-02', '2023-02-01', '2023-02-02'],
854 dtype='datetime64[ns]', freq=None)
855 >>> idx.snap("MS")
856 DatetimeIndex(['2023-01-01', '2023-01-01', '2023-02-01', '2023-02-01'],
857 dtype='datetime64[ns]', freq=None)
858 """
859 # Superdumb, punting on any optimizing
860 freq = to_offset(freq)
861
862 dta = self._data.copy()
863
864 for i, v in enumerate(self):
865 s = v
866 if not freq.is_on_offset(s):
867 t0 = freq.rollback(s)
868 t1 = freq.rollforward(s)
869 if abs(s - t0) < abs(t1 - s):
870 s = t0
871 else:
872 s = t1
873 dta[i] = s
874
875 return DatetimeIndex._simple_new(dta, name=self.name)
876
877 # --------------------------------------------------------------------
878 # Indexing Methods
879
880 def _parsed_string_to_bounds(
881 self, reso: Resolution, parsed: dt.datetime
882 ) -> tuple[Timestamp, Timestamp]:
883 """
884 Calculate datetime bounds for parsed time string and its resolution.
885
886 Parameters
887 ----------
888 reso : Resolution
889 Resolution provided by parsed string.
890 parsed : datetime
891 Datetime from parsed string.
892
893 Returns
894 -------
895 lower, upper: pd.Timestamp
896 """
897 freq = OFFSET_TO_PERIOD_FREQSTR.get(reso.attr_abbrev, reso.attr_abbrev)
898 per = Period(parsed, freq=freq)
899 start = per.start_time
900 # Can't use end_time here bc that will subtract a microsecond
901 # instead of a nanosecond
902 end = (per + 1).start_time - np.timedelta64(1, "ns")
903 start = start.as_unit(self.unit)
904 end = end.as_unit(self.unit)
905
906 # GH 24076
907 # If an incoming date string contained a UTC offset, need to localize
908 # the parsed date to this offset first before aligning with the index's
909 # timezone
910 start = start.tz_localize(parsed.tzinfo)
911 end = end.tz_localize(parsed.tzinfo)
912
913 if parsed.tzinfo is not None:
914 if self.tz is None:
915 raise ValueError(
916 "The index must be timezone aware when indexing "
917 "with a date string with a UTC offset"
918 )
919 # The flipped case with parsed.tz is None and self.tz is not None
920 # is ruled out bc parsed and reso are produced by _parse_with_reso,
921 # which localizes parsed.
922 return start, end
923
924 def _parse_with_reso(self, label: str) -> tuple[Timestamp, Resolution]:
925 parsed, reso = super()._parse_with_reso(label)
926
927 parsed = Timestamp(parsed)
928
929 if self.tz is not None and parsed.tzinfo is None:
930 # we special-case timezone-naive strings and timezone-aware
931 # DatetimeIndex
932 # https://github.com/pandas-dev/pandas/pull/36148#issuecomment-687883081
933 parsed = parsed.tz_localize(self.tz)
934
935 return parsed, reso
936
937 def _disallow_mismatched_indexing(self, key) -> None:
938 """
939 Check for mismatched-tzawareness indexing and re-raise as KeyError.
940 """
941 # we get here with isinstance(key, self._data._recognized_scalars)
942 try:
943 # GH#36148
944 self._data._assert_tzawareness_compat(key)
945 except TypeError as err:
946 raise KeyError(key) from err
947
948 def get_loc(self, key):
949 """
950 Get integer location for requested label
951
952 Returns
953 -------
954 loc : int
955 """
956 self._check_indexing_error(key)
957
958 orig_key = key
959 if is_valid_na_for_dtype(key, self.dtype):
960 key = NaT
961
962 if isinstance(key, self._data._recognized_scalars):
963 # needed to localize naive datetimes
964 self._disallow_mismatched_indexing(key)
965 key = Timestamp(key)
966
967 elif isinstance(key, str):
968 try:
969 parsed, reso = self._parse_with_reso(key)
970 except ValueError as err:
971 raise KeyError(key) from err
972 self._disallow_mismatched_indexing(parsed)
973
974 if self._can_partial_date_slice(reso):
975 try:
976 return self._partial_date_slice(reso, parsed)
977 except KeyError as err:
978 raise KeyError(key) from err
979
980 key = parsed
981
982 elif isinstance(key, dt.timedelta):
983 # GH#20464
984 raise TypeError(
985 f"Cannot index {type(self).__name__} with {type(key).__name__}"
986 )
987
988 elif isinstance(key, dt.time):
989 return self.indexer_at_time(key)
990
991 else:
992 # unrecognized type
993 raise KeyError(key)
994
995 try:
996 return Index.get_loc(self, key)
997 except KeyError as err:
998 raise KeyError(orig_key) from err
999
1000 def _maybe_cast_slice_bound(self, label, side: str):
1001 """
1002 This function should be overloaded in subclasses that allow non-trivial
1003 casting on label-slice bounds, e.g. datetime-like indices allowing
1004 strings containing formatted datetimes.
1005
1006 Parameters
1007 ----------
1008 label : object
1009 side : {'left', 'right'}
1010
1011 Returns
1012 -------
1013 label : object
1014
1015 Notes
1016 -----
1017 Value of `side` parameter should be validated in caller.
1018 """
1019 # GH#42855 handle date here instead of get_slice_bound
1020 if isinstance(label, dt.date) and not isinstance(label, dt.datetime):
1021 # Pandas supports slicing with dates, treated as datetimes at midnight.
1022 # https://github.com/pandas-dev/pandas/issues/31501
1023 label = Timestamp(label).to_pydatetime()
1024 warnings.warn(
1025 # GH#35830 deprecate last remaining inconsistent date treatment
1026 "Slicing with a datetime.date object is deprecated. "
1027 "Explicitly cast to Timestamp instead.",
1028 Pandas4Warning,
1029 stacklevel=find_stack_level(),
1030 )
1031
1032 label = super()._maybe_cast_slice_bound(label, side)
1033 self._data._assert_tzawareness_compat(label)
1034 return Timestamp(label)
1035
1036 def slice_indexer(self, start=None, end=None, step=None):
1037 """
1038 Return indexer for specified label slice.
1039 Index.slice_indexer, customized to handle time slicing.
1040
1041 In addition to functionality provided by Index.slice_indexer, does the
1042 following:
1043
1044 - if both `start` and `end` are instances of `datetime.time`, it
1045 invokes `indexer_between_time`
1046 - if `start` and `end` are both either string or None perform
1047 value-based selection in non-monotonic cases.
1048
1049 """
1050 # For historical reasons DatetimeIndex supports slices between two
1051 # instances of datetime.time as if it were applying a slice mask to
1052 # an array of (self.hour, self.minute, self.seconds, self.microsecond).
1053 if isinstance(start, dt.time) and isinstance(end, dt.time):
1054 if step is not None and step != 1:
1055 raise ValueError("Must have step size of 1 with time slices")
1056 return self.indexer_between_time(start, end)
1057
1058 if isinstance(start, dt.time) or isinstance(end, dt.time):
1059 raise KeyError("Cannot mix time and non-time slice keys")
1060
1061 def check_str_or_none(point) -> bool:
1062 return point is not None and not isinstance(point, str)
1063
1064 # GH#33146 if start and end are combinations of str and None and Index is not
1065 # monotonic, we can not use Index.slice_indexer because it does not honor the
1066 # actual elements, is only searching for start and end
1067 if (
1068 check_str_or_none(start)
1069 or check_str_or_none(end)
1070 or self.is_monotonic_increasing
1071 ):
1072 return Index.slice_indexer(self, start, end, step)
1073
1074 mask = np.array(True)
1075 in_index = True
1076 if start is not None:
1077 start_casted = self._maybe_cast_slice_bound(start, "left")
1078 mask = start_casted <= self
1079 in_index &= (start_casted == self).any()
1080
1081 if end is not None:
1082 end_casted = self._maybe_cast_slice_bound(end, "right")
1083 mask = (self <= end_casted) & mask
1084 in_index &= (end_casted == self).any()
1085
1086 if not in_index:
1087 raise KeyError(
1088 "Value based partial slicing on non-monotonic DatetimeIndexes "
1089 "with non-existing keys is not allowed.",
1090 )
1091 indexer = mask.nonzero()[0][::step]
1092 if len(indexer) == len(self):
1093 return slice(None)
1094 else:
1095 return indexer
1096
1097 # --------------------------------------------------------------------
1098
1099 @property
1100 def inferred_type(self) -> str:
1101 # b/c datetime is represented as microseconds since the epoch, make
1102 # sure we can't have ambiguous indexing
1103 return "datetime64"
1104
1105 def indexer_at_time(self, time, asof: bool = False) -> npt.NDArray[np.intp]:
1106 """
1107 Return index locations of values at particular time of day.
1108
1109 Parameters
1110 ----------
1111 time : datetime.time or str
1112 Time passed in either as object (datetime.time) or as string in
1113 appropriate format ("%H:%M", "%H%M", "%I:%M%p", "%I%M%p",
1114 "%H:%M:%S", "%H%M%S", "%I:%M:%S%p", "%I%M%S%p").
1115 asof : bool, default False
1116 This parameter is currently not supported.
1117
1118 Returns
1119 -------
1120 np.ndarray[np.intp]
1121 Index locations of values at given `time` of day.
1122
1123 See Also
1124 --------
1125 indexer_between_time : Get index locations of values between particular
1126 times of day.
1127 DataFrame.at_time : Select values at particular time of day.
1128
1129 Examples
1130 --------
1131 >>> idx = pd.DatetimeIndex(
1132 ... ["1/1/2020 10:00", "2/1/2020 11:00", "3/1/2020 10:00"]
1133 ... )
1134 >>> idx.indexer_at_time("10:00")
1135 array([0, 2])
1136 """
1137 if asof:
1138 raise NotImplementedError("'asof' argument is not supported")
1139
1140 if isinstance(time, str):
1141 from dateutil.parser import parse
1142
1143 orig = time
1144 try:
1145 alt = to_time(time)
1146 except ValueError:
1147 warnings.warn(
1148 # GH#50839
1149 f"The string '{orig}' cannot be parsed using pd.core.tools.to_time "
1150 f"and in a future version will raise. "
1151 "Use an unambiguous time string format or explicitly cast to "
1152 "`datetime.time` before calling.",
1153 Pandas4Warning,
1154 stacklevel=find_stack_level(),
1155 )
1156 time = parse(time).time()
1157 else:
1158 try:
1159 time = parse(time).time()
1160 except ValueError:
1161 # e.g. '23550' raises dateutil.parser._parser.ParserError
1162 time = alt
1163 if alt != time:
1164 warnings.warn(
1165 # GH#50839
1166 f"The string '{orig}' is currently parsed as {time} "
1167 f"but in a future version will be parsed as {alt}, consistent"
1168 "with `between_time` behavior. To avoid this warning, "
1169 "use an unambiguous string format or explicitly cast to "
1170 "`datetime.time` before calling.",
1171 Pandas4Warning,
1172 stacklevel=find_stack_level(),
1173 )
1174
1175 if time.tzinfo:
1176 if self.tz is None:
1177 raise ValueError("Index must be timezone aware.")
1178 time_micros = self.tz_convert(time.tzinfo)._get_time_micros()
1179 else:
1180 time_micros = self._get_time_micros()
1181 micros = _time_to_micros(time)
1182 return (time_micros == micros).nonzero()[0]
1183
1184 def indexer_between_time(
1185 self, start_time, end_time, include_start: bool = True, include_end: bool = True
1186 ) -> npt.NDArray[np.intp]:
1187 """
1188 Return index locations of values between particular times of day.
1189
1190 Parameters
1191 ----------
1192 start_time, end_time : datetime.time, str
1193 Time passed either as object (datetime.time) or as string in
1194 appropriate format ("%H:%M", "%H%M", "%I:%M%p", "%I%M%p",
1195 "%H:%M:%S", "%H%M%S", "%I:%M:%S%p","%I%M%S%p").
1196 include_start : bool, default True
1197 Include boundaries; whether to set start bound as closed or open.
1198 include_end : bool, default True
1199 Include boundaries; whether to set end bound as closed or open.
1200
1201 Returns
1202 -------
1203 np.ndarray[np.intp]
1204 Index locations of values between particular times of day.
1205
1206 See Also
1207 --------
1208 indexer_at_time : Get index locations of values at particular time of day.
1209 DataFrame.between_time : Select values between particular times of day.
1210
1211 Examples
1212 --------
1213 >>> idx = pd.date_range("2023-01-01", periods=4, freq="h")
1214 >>> idx
1215 DatetimeIndex(['2023-01-01 00:00:00', '2023-01-01 01:00:00',
1216 '2023-01-01 02:00:00', '2023-01-01 03:00:00'],
1217 dtype='datetime64[us]', freq='h')
1218 >>> idx.indexer_between_time("00:00", "2:00", include_end=False)
1219 array([0, 1])
1220 """
1221 start_time = to_time(start_time)
1222 end_time = to_time(end_time)
1223 time_micros = self._get_time_micros()
1224 start_micros = _time_to_micros(start_time)
1225 end_micros = _time_to_micros(end_time)
1226
1227 if include_start and include_end:
1228 lop = rop = operator.le
1229 elif include_start:
1230 lop = operator.le
1231 rop = operator.lt
1232 elif include_end:
1233 lop = operator.lt
1234 rop = operator.le
1235 else:
1236 lop = rop = operator.lt
1237
1238 if start_time <= end_time:
1239 join_op = operator.and_
1240 else:
1241 join_op = operator.or_
1242
1243 mask = join_op(lop(start_micros, time_micros), rop(time_micros, end_micros))
1244
1245 return mask.nonzero()[0]
1246
1247
1248@set_module("pandas")
1249def date_range(
1250 start=None,
1251 end=None,
1252 periods=None,
1253 freq=None,
1254 tz=None,
1255 normalize: bool = False,
1256 name: Hashable | None = None,
1257 inclusive: IntervalClosedType = "both",
1258 *,
1259 unit: TimeUnit | None = None,
1260 **kwargs,
1261) -> DatetimeIndex:
1262 """
1263 Return a fixed frequency DatetimeIndex.
1264
1265 Returns the range of equally spaced time points (where the difference between any
1266 two adjacent points is specified by the given frequency) such that they fall in the
1267 range `[start, end]` , where the first one and the last one are, resp., the first
1268 and last time points in that range that fall on the boundary of ``freq`` (if given
1269 as a frequency string) or that are valid for ``freq`` (if given as a
1270 :class:`pandas.tseries.offsets.DateOffset`). If ``freq`` is positive, the points
1271 satisfy `start <[=] x <[=] end`, and if ``freq`` is negative, the points satisfy
1272 `end <[=] x <[=] start`. (If exactly one of ``start``, ``end``, or ``freq`` is *not*
1273 specified, this missing parameter can be computed given ``periods``, the number of
1274 timesteps in the range. See the note below.)
1275
1276 Parameters
1277 ----------
1278 start : str or datetime-like, optional
1279 Left bound for generating dates.
1280 end : str or datetime-like, optional
1281 Right bound for generating dates.
1282 periods : int, optional
1283 Number of periods to generate.
1284 freq : str, Timedelta, datetime.timedelta, or DateOffset, default 'D'
1285 Frequency strings can have multiples, e.g. '5h'. See
1286 :ref:`here <timeseries.offset_aliases>` for a list of
1287 frequency aliases.
1288 tz : str or tzinfo, optional
1289 Time zone name for returning localized DatetimeIndex, for example
1290 'Asia/Hong_Kong'. By default, the resulting DatetimeIndex is
1291 timezone-naive unless timezone-aware datetime-likes are passed.
1292 normalize : bool, default False
1293 Normalize start/end dates to midnight before generating date range.
1294 name : Hashable, default None
1295 Name of the resulting DatetimeIndex.
1296 inclusive : {"both", "neither", "left", "right"}, default "both"
1297 Include boundaries; Whether to set each bound as closed or open.
1298 unit : {'s', 'ms', 'us', 'ns', None}, default None
1299 Specify the desired resolution of the result.
1300 If not specified, this is inferred from the 'start', 'end', and 'freq'
1301 using the same inference as :class:`Timestamp` taking the highest
1302 resolution of the three that are provided.
1303
1304 .. versionadded:: 2.0.0
1305 **kwargs
1306 For compatibility. Has no effect on the result.
1307
1308 Returns
1309 -------
1310 DatetimeIndex
1311 A DatetimeIndex object of the generated dates.
1312
1313 See Also
1314 --------
1315 DatetimeIndex : An immutable container for datetimes.
1316 timedelta_range : Return a fixed frequency TimedeltaIndex.
1317 period_range : Return a fixed frequency PeriodIndex.
1318 interval_range : Return a fixed frequency IntervalIndex.
1319
1320 Notes
1321 -----
1322 Of the four parameters ``start``, ``end``, ``periods``, and ``freq``,
1323 a maximum of three can be specified at once. Of the three parameters
1324 ``start``, ``end``, and ``periods``, at least two must be specified.
1325 If ``freq`` is omitted, the resulting ``DatetimeIndex`` will have
1326 ``periods`` linearly spaced elements between ``start`` and ``end``
1327 (closed on both sides).
1328
1329 To learn more about the frequency strings, please see
1330 :ref:`this link<timeseries.offset_aliases>`.
1331
1332 Examples
1333 --------
1334 **Specifying the values**
1335
1336 The next four examples generate the same `DatetimeIndex`, but vary
1337 the combination of `start`, `end` and `periods`.
1338
1339 Specify `start` and `end`, with the default daily frequency.
1340
1341 >>> pd.date_range(start="1/1/2018", end="1/08/2018")
1342 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
1343 '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08'],
1344 dtype='datetime64[us]', freq='D')
1345
1346 Specify timezone-aware `start` and `end`, with the default daily frequency.
1347
1348 >>> pd.date_range(
1349 ... start=pd.to_datetime("1/1/2018").tz_localize("Europe/Berlin"),
1350 ... end=pd.to_datetime("1/08/2018").tz_localize("Europe/Berlin"),
1351 ... )
1352 DatetimeIndex(['2018-01-01 00:00:00+01:00', '2018-01-02 00:00:00+01:00',
1353 '2018-01-03 00:00:00+01:00', '2018-01-04 00:00:00+01:00',
1354 '2018-01-05 00:00:00+01:00', '2018-01-06 00:00:00+01:00',
1355 '2018-01-07 00:00:00+01:00', '2018-01-08 00:00:00+01:00'],
1356 dtype='datetime64[us, Europe/Berlin]', freq='D')
1357
1358 Specify `start` and `periods`, the number of periods (days).
1359
1360 >>> pd.date_range(start="1/1/2018", periods=8)
1361 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
1362 '2018-01-05', '2018-01-06', '2018-01-07', '2018-01-08'],
1363 dtype='datetime64[us]', freq='D')
1364
1365 Specify `end` and `periods`, the number of periods (days).
1366
1367 >>> pd.date_range(end="1/1/2018", periods=8)
1368 DatetimeIndex(['2017-12-25', '2017-12-26', '2017-12-27', '2017-12-28',
1369 '2017-12-29', '2017-12-30', '2017-12-31', '2018-01-01'],
1370 dtype='datetime64[us]', freq='D')
1371
1372 Specify `start`, `end`, and `periods`; the frequency is generated
1373 automatically (linearly spaced).
1374
1375 >>> pd.date_range(start="2018-04-24", end="2018-04-27", periods=3)
1376 DatetimeIndex(['2018-04-24 00:00:00', '2018-04-25 12:00:00',
1377 '2018-04-27 00:00:00'],
1378 dtype='datetime64[us]', freq=None)
1379
1380 **Other Parameters**
1381
1382 Changed the `freq` (frequency) to ``'ME'`` (month end frequency).
1383
1384 >>> pd.date_range(start="1/1/2018", periods=5, freq="ME")
1385 DatetimeIndex(['2018-01-31', '2018-02-28', '2018-03-31', '2018-04-30',
1386 '2018-05-31'],
1387 dtype='datetime64[us]', freq='ME')
1388
1389 Multiples are allowed
1390
1391 >>> pd.date_range(start="1/1/2018", periods=5, freq="3ME")
1392 DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31',
1393 '2019-01-31'],
1394 dtype='datetime64[us]', freq='3ME')
1395
1396 `freq` can also be specified as an Offset object.
1397
1398 >>> pd.date_range(start="1/1/2018", periods=5, freq=pd.offsets.MonthEnd(3))
1399 DatetimeIndex(['2018-01-31', '2018-04-30', '2018-07-31', '2018-10-31',
1400 '2019-01-31'],
1401 dtype='datetime64[us]', freq='3ME')
1402
1403 Specify `tz` to set the timezone.
1404
1405 >>> pd.date_range(start="1/1/2018", periods=5, tz="Asia/Tokyo")
1406 DatetimeIndex(['2018-01-01 00:00:00+09:00', '2018-01-02 00:00:00+09:00',
1407 '2018-01-03 00:00:00+09:00', '2018-01-04 00:00:00+09:00',
1408 '2018-01-05 00:00:00+09:00'],
1409 dtype='datetime64[us, Asia/Tokyo]', freq='D')
1410
1411 `inclusive` controls whether to include `start` and `end` that are on the
1412 boundary. The default, "both", includes boundary points on either end.
1413
1414 >>> pd.date_range(start="2017-01-01", end="2017-01-04", inclusive="both")
1415 DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03', '2017-01-04'],
1416 dtype='datetime64[us]', freq='D')
1417
1418 Use ``inclusive='left'`` to exclude `end` if it falls on the boundary.
1419
1420 >>> pd.date_range(start="2017-01-01", end="2017-01-04", inclusive="left")
1421 DatetimeIndex(['2017-01-01', '2017-01-02', '2017-01-03'],
1422 dtype='datetime64[us]', freq='D')
1423
1424 Use ``inclusive='right'`` to exclude `start` if it falls on the boundary, and
1425 similarly ``inclusive='neither'`` will exclude both `start` and `end`.
1426
1427 >>> pd.date_range(start="2017-01-01", end="2017-01-04", inclusive="right")
1428 DatetimeIndex(['2017-01-02', '2017-01-03', '2017-01-04'],
1429 dtype='datetime64[us]', freq='D')
1430
1431 **Specify a unit**
1432
1433 >>> pd.date_range(start="2017-01-01", periods=10, freq="100YS", unit="s")
1434 DatetimeIndex(['2017-01-01', '2117-01-01', '2217-01-01', '2317-01-01',
1435 '2417-01-01', '2517-01-01', '2617-01-01', '2717-01-01',
1436 '2817-01-01', '2917-01-01'],
1437 dtype='datetime64[s]', freq='100YS-JAN')
1438 """
1439 if freq is None and com.any_none(periods, start, end):
1440 freq = "D"
1441 if freq is not None:
1442 freq = to_offset(freq)
1443
1444 if start is NaT or end is NaT:
1445 # This check needs to come before the `unit = start.unit` line below
1446 raise ValueError("Neither `start` nor `end` can be NaT")
1447
1448 if unit is None:
1449 # Infer the unit based on the inputs
1450
1451 if start is not None and end is not None:
1452 start = Timestamp(start)
1453 end = Timestamp(end)
1454 if abbrev_to_npy_unit(start.unit) > abbrev_to_npy_unit(end.unit):
1455 unit = start.unit
1456 else:
1457 unit = end.unit
1458 elif start is not None:
1459 start = Timestamp(start)
1460 unit = start.unit
1461 elif end is not None:
1462 end = Timestamp(end)
1463 unit = end.unit
1464 else:
1465 raise ValueError(
1466 "Of the four parameters: start, end, periods, "
1467 "and freq, exactly three must be specified"
1468 )
1469
1470 # Last we need to watch out for cases where the 'freq' implies a higher
1471 # unit than either start or end
1472 if freq is not None:
1473 creso = abbrev_to_npy_unit(unit)
1474 if isinstance(freq, Tick):
1475 if freq._creso > creso:
1476 unit = freq.base.freqstr # type: ignore[assignment]
1477 elif hasattr(freq, "offset") and freq.offset is not None:
1478 # e.g. BDay with an offset
1479 td = Timedelta(freq.offset)
1480 if abbrev_to_npy_unit(td.unit) > creso:
1481 unit = td.unit
1482 elif type(freq) is DateOffset:
1483 if getattr(freq, "nanoseconds", 0) != 0:
1484 # e.g. test_freq_dateoffset_with_relateivedelta_nanos
1485 unit = "ns"
1486 elif getattr(freq, "microseconds", 0) != 0 and unit != "ns":
1487 unit = "us"
1488 elif getattr(freq, "milliseconds", 0) != 0 and unit not in ["ns", "us"]:
1489 unit = "ms"
1490
1491 dtarr = DatetimeArray._generate_range(
1492 start=start,
1493 end=end,
1494 periods=periods,
1495 freq=freq,
1496 tz=tz,
1497 normalize=normalize,
1498 inclusive=inclusive,
1499 unit=unit,
1500 **kwargs,
1501 )
1502 return DatetimeIndex._simple_new(dtarr, name=name)
1503
1504
1505@set_module("pandas")
1506def bdate_range(
1507 start=None,
1508 end=None,
1509 periods: int | None = None,
1510 freq: Frequency | dt.timedelta = "B",
1511 tz=None,
1512 normalize: bool = True,
1513 name: Hashable | None = None,
1514 weekmask=None,
1515 holidays=None,
1516 inclusive: IntervalClosedType = "both",
1517 **kwargs,
1518) -> DatetimeIndex:
1519 """
1520 Return a fixed frequency DatetimeIndex with business day as the default.
1521
1522 Parameters
1523 ----------
1524 start : str or datetime-like, default None
1525 Left bound for generating dates.
1526 end : str or datetime-like, default None
1527 Right bound for generating dates.
1528 periods : int, default None
1529 Number of periods to generate.
1530 freq : str, Timedelta, datetime.timedelta, or DateOffset, default 'B'
1531 Frequency strings can have multiples, e.g. '5h'. The default is
1532 business daily ('B').
1533 tz : str or None
1534 Time zone name for returning localized DatetimeIndex, for example
1535 Asia/Beijing.
1536 normalize : bool, default False
1537 Normalize start/end dates to midnight before generating date range.
1538 name : Hashable, default None
1539 Name of the resulting DatetimeIndex.
1540 weekmask : str or None, default None
1541 Weekmask of valid business days, passed to ``numpy.busdaycalendar``,
1542 only used when custom frequency strings are passed. The default
1543 value None is equivalent to 'Mon Tue Wed Thu Fri'.
1544 holidays : list-like or None, default None
1545 Dates to exclude from the set of valid business days, passed to
1546 ``numpy.busdaycalendar``, only used when custom frequency strings
1547 are passed.
1548 inclusive : {"both", "neither", "left", "right"}, default "both"
1549 Include boundaries; Whether to set each bound as closed or open.
1550 **kwargs
1551 For compatibility. Has no effect on the result.
1552
1553 Returns
1554 -------
1555 DatetimeIndex
1556 Fixed frequency DatetimeIndex.
1557
1558 See Also
1559 --------
1560 date_range : Return a fixed frequency DatetimeIndex.
1561 period_range : Return a fixed frequency PeriodIndex.
1562 timedelta_range : Return a fixed frequency TimedeltaIndex.
1563
1564 Notes
1565 -----
1566 Of the four parameters: ``start``, ``end``, ``periods``, and ``freq``,
1567 exactly three must be specified. Specifying ``freq`` is a requirement
1568 for ``bdate_range``. Use ``date_range`` if specifying ``freq`` is not
1569 desired.
1570
1571 To learn more about the frequency strings, please see
1572 :ref:`this link<timeseries.offset_aliases>`.
1573
1574 Examples
1575 --------
1576 Note how the two weekend days are skipped in the result.
1577
1578 >>> pd.bdate_range(start="1/1/2018", end="1/08/2018")
1579 DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
1580 '2018-01-05', '2018-01-08'],
1581 dtype='datetime64[us]', freq='B')
1582 """
1583 if freq is None:
1584 msg = "freq must be specified for bdate_range; use date_range instead"
1585 raise TypeError(msg)
1586
1587 if isinstance(freq, str) and freq.upper().startswith("C"):
1588 msg = f"invalid custom frequency string: {freq}"
1589 if freq == "CBH":
1590 raise ValueError(f"{msg}, did you mean cbh?")
1591 try:
1592 weekmask = weekmask or "Mon Tue Wed Thu Fri"
1593 freq = prefix_mapping[freq](holidays=holidays, weekmask=weekmask)
1594 except (KeyError, TypeError) as err:
1595 raise ValueError(msg) from err
1596 elif holidays or weekmask:
1597 msg = (
1598 "a custom frequency string is required when holidays or "
1599 f"weekmask are passed, got frequency {freq}"
1600 )
1601 raise ValueError(msg)
1602
1603 return date_range(
1604 start=start,
1605 end=end,
1606 periods=periods,
1607 freq=freq,
1608 tz=tz,
1609 normalize=normalize,
1610 name=name,
1611 inclusive=inclusive,
1612 **kwargs,
1613 )
1614
1615
1616def _time_to_micros(time_obj: dt.time) -> int:
1617 seconds = time_obj.hour * 60 * 60 + 60 * time_obj.minute + time_obj.second
1618 return 1_000_000 * seconds + time_obj.microsecond