1"""implement the TimedeltaIndex"""
2
3from __future__ import annotations
4
5from typing import (
6 TYPE_CHECKING,
7 cast,
8)
9
10from pandas._libs import (
11 index as libindex,
12 lib,
13)
14from pandas._libs.tslibs import (
15 Resolution,
16 Timedelta,
17 to_offset,
18)
19from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
20from pandas.util._decorators import set_module
21
22from pandas.core.dtypes.common import (
23 is_scalar,
24 pandas_dtype,
25)
26from pandas.core.dtypes.dtypes import ArrowDtype
27from pandas.core.dtypes.generic import ABCSeries
28
29from pandas.core.arrays.timedeltas import TimedeltaArray
30import pandas.core.common as com
31from pandas.core.indexes.base import (
32 Index,
33 maybe_extract_name,
34)
35from pandas.core.indexes.datetimelike import DatetimeTimedeltaMixin
36from pandas.core.indexes.extension import inherit_names
37
38if TYPE_CHECKING:
39 from pandas._libs import NaTType
40 from pandas._libs.tslibs import (
41 Day,
42 Tick,
43 )
44 from pandas._typing import (
45 DtypeObj,
46 TimeUnit,
47 )
48
49
50@inherit_names(
51 [
52 "__neg__",
53 "__pos__",
54 "__abs__",
55 "total_seconds",
56 "round",
57 "floor",
58 "ceil",
59 *TimedeltaArray._field_ops,
60 ],
61 TimedeltaArray,
62 wrap=True,
63)
64@inherit_names(
65 [
66 "components",
67 "to_pytimedelta",
68 "sum",
69 "std",
70 "median",
71 ],
72 TimedeltaArray,
73)
74@set_module("pandas")
75class TimedeltaIndex(DatetimeTimedeltaMixin):
76 """
77 Immutable Index of timedelta64 data.
78
79 Represented internally as int64, and scalars returned Timedelta objects.
80
81 Parameters
82 ----------
83 data : array-like (1-dimensional), optional
84 Optional timedelta-like data to construct index with.
85 freq : str or pandas offset object, optional
86 One of pandas date offset strings or corresponding objects. The string
87 ``'infer'`` can be passed in order to set the frequency of the index as
88 the inferred frequency upon creation.
89 dtype : numpy.dtype or str, default None
90 Valid ``numpy`` dtypes are ``timedelta64[ns]``, ``timedelta64[us]``,
91 ``timedelta64[ms]``, and ``timedelta64[s]``.
92 copy : bool, default None
93 Whether to copy input data, only relevant for array, Series, and Index
94 inputs (for other input, e.g. a list, a new array is created anyway).
95 Defaults to True for array input and False for Index/Series.
96 Set to False to avoid copying array input at your own risk (if you
97 know the input data won't be modified elsewhere).
98 Set to True to force copying Series/Index input up front.
99 name : object
100 Name to be stored in the index.
101
102 Attributes
103 ----------
104 days
105 seconds
106 microseconds
107 nanoseconds
108 components
109 inferred_freq
110
111 Methods
112 -------
113 to_pytimedelta
114 to_series
115 round
116 floor
117 ceil
118 to_frame
119 mean
120
121 See Also
122 --------
123 Index : The base pandas Index type.
124 Timedelta : Represents a duration between two dates or times.
125 DatetimeIndex : Index of datetime64 data.
126 PeriodIndex : Index of Period data.
127 timedelta_range : Create a fixed-frequency TimedeltaIndex.
128
129 Notes
130 -----
131 To learn more about the frequency strings, please see
132 :ref:`this link<timeseries.offset_aliases>`.
133
134 Examples
135 --------
136 >>> pd.TimedeltaIndex(["0 days", "1 days", "2 days", "3 days", "4 days"])
137 TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
138 dtype='timedelta64[us]', freq=None)
139
140 We can also let pandas infer the frequency when possible.
141
142 >>> pd.TimedeltaIndex(np.arange(5) * 24 * 3600 * 1e9, freq="infer")
143 TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
144 dtype='timedelta64[ns]', freq='D')
145 """
146
147 _typ = "timedeltaindex"
148
149 _data_cls = TimedeltaArray
150
151 @property
152 def _engine_type(self) -> type[libindex.TimedeltaEngine]:
153 return libindex.TimedeltaEngine
154
155 _data: TimedeltaArray
156
157 # Use base class method instead of DatetimeTimedeltaMixin._get_string_slice
158 _get_string_slice = Index._get_string_slice
159
160 # error: Signature of "_resolution_obj" incompatible with supertype
161 # "DatetimeIndexOpsMixin"
162 @property
163 def _resolution_obj(self) -> Resolution | None: # type: ignore[override]
164 return self._data._resolution_obj
165
166 # -------------------------------------------------------------------
167 # Constructors
168
169 def __new__(
170 cls,
171 data=None,
172 freq=lib.no_default,
173 dtype=None,
174 copy: bool | None = None,
175 name=None,
176 ):
177 name = maybe_extract_name(name, data, cls)
178
179 # GH#63388
180 data, copy = cls._maybe_copy_array_input(data, copy, dtype)
181
182 if is_scalar(data):
183 cls._raise_scalar_data_error(data)
184
185 if dtype is not None:
186 dtype = pandas_dtype(dtype)
187
188 if (
189 isinstance(data, TimedeltaArray)
190 and freq is lib.no_default
191 and (dtype is None or dtype == data.dtype)
192 ):
193 if copy:
194 data = data.copy()
195 return cls._simple_new(data, name=name)
196
197 if (
198 isinstance(data, TimedeltaIndex)
199 and freq is lib.no_default
200 and name is None
201 and (dtype is None or dtype == data.dtype)
202 ):
203 if copy:
204 return data.copy()
205 else:
206 return data._view()
207
208 # - Cases checked above all return/raise before reaching here - #
209
210 tdarr = TimedeltaArray._from_sequence_not_strict(
211 data, freq=freq, unit=None, dtype=dtype, copy=copy
212 )
213 refs = None
214 if not copy and isinstance(data, (ABCSeries, Index)):
215 refs = data._references
216
217 return cls._simple_new(tdarr, name=name, refs=refs)
218
219 # -------------------------------------------------------------------
220
221 def _is_comparable_dtype(self, dtype: DtypeObj) -> bool:
222 """
223 Can we compare values of the given dtype to our own?
224 """
225 if isinstance(dtype, ArrowDtype):
226 return dtype.kind == "m"
227 return lib.is_np_dtype(dtype, "m") # aka self._data._is_recognized_dtype
228
229 # -------------------------------------------------------------------
230 # Indexing Methods
231
232 def get_loc(self, key):
233 """
234 Get integer location for requested label
235
236 Returns
237 -------
238 loc : int, slice, or ndarray[int]
239 """
240 self._check_indexing_error(key)
241
242 try:
243 key = self._data._validate_scalar(key, unbox=False)
244 except TypeError as err:
245 raise KeyError(key) from err
246
247 return Index.get_loc(self, key)
248
249 # error: Return type "tuple[Timedelta | NaTType, Resolution]" of
250 # "_parse_with_reso" incompatible with return type
251 # "tuple[datetime, Resolution]" in supertype
252 # "pandas.core.indexes.datetimelike.DatetimeIndexOpsMixin"
253 def _parse_with_reso(self, label: str) -> tuple[Timedelta | NaTType, Resolution]: # type: ignore[override]
254 parsed = Timedelta(label)
255 if isinstance(parsed, Timedelta):
256 reso = Resolution.get_reso_from_freqstr(parsed.unit)
257 else:
258 # i.e. pd.NaT
259 reso = Resolution.get_reso_from_freqstr("s")
260 return parsed, reso
261
262 def _parsed_string_to_bounds(self, reso: Resolution, parsed: Timedelta):
263 # reso is unused, included to match signature of DTI/PI
264 lbound = parsed.round(parsed.resolution_string)
265 rbound = (
266 lbound
267 + to_offset(parsed.resolution_string)
268 - Timedelta(1, unit=self.unit).as_unit(self.unit)
269 )
270 return lbound, rbound
271
272 # -------------------------------------------------------------------
273
274 @property
275 def inferred_type(self) -> str:
276 return "timedelta64"
277
278
279@set_module("pandas")
280def timedelta_range(
281 start=None,
282 end=None,
283 periods: int | None = None,
284 freq=None,
285 name=None,
286 closed=None,
287 *,
288 unit: TimeUnit | None = None,
289) -> TimedeltaIndex:
290 """
291 Return a fixed frequency TimedeltaIndex with day as the default.
292
293 Parameters
294 ----------
295 start : str or timedelta-like, default None
296 Left bound for generating timedeltas.
297 end : str or timedelta-like, default None
298 Right bound for generating timedeltas.
299 periods : int, default None
300 Number of periods to generate.
301 freq : str, Timedelta, datetime.timedelta, or DateOffset, default 'D'
302 Frequency strings can have multiples, e.g. '5h'.
303 name : Hashable, default None
304 Name of the resulting TimedeltaIndex.
305 closed : str, default None
306 Make the interval closed with respect to the given frequency to
307 the 'left', 'right', or both sides (None).
308 unit : {'s', 'ms', 'us', 'ns', None}, default None
309 Specify the desired resolution of the result.
310 If not specified, this is inferred from the 'start', 'end', and 'freq'
311 using the same inference as :class:`Timedelta` taking the highest
312 resolution of the three that are provided.
313
314 .. versionadded:: 2.0.0
315
316 Returns
317 -------
318 TimedeltaIndex
319 Fixed frequency, with day as the default.
320
321 See Also
322 --------
323 date_range : Return a fixed frequency DatetimeIndex.
324 period_range : Return a fixed frequency PeriodIndex.
325
326 Notes
327 -----
328 Of the four parameters ``start``, ``end``, ``periods``, and ``freq``,
329 a maximum of three can be specified at once. Of the three parameters
330 ``start``, ``end``, and ``periods``, at least two must be specified.
331 If ``freq`` is omitted, the resulting ``DatetimeIndex`` will have
332 ``periods`` linearly spaced elements between ``start`` and ``end``
333 (closed on both sides).
334
335 To learn more about the frequency strings, please see
336 :ref:`this link<timeseries.offset_aliases>`.
337
338 Examples
339 --------
340 >>> pd.timedelta_range(start="1 day", periods=4)
341 TimedeltaIndex(['1 days', '2 days', '3 days', '4 days'],
342 dtype='timedelta64[us]', freq='D')
343
344 The ``closed`` parameter specifies which endpoint is included. The default
345 behavior is to include both endpoints.
346
347 >>> pd.timedelta_range(start="1 day", periods=4, closed="right")
348 TimedeltaIndex(['2 days', '3 days', '4 days'],
349 dtype='timedelta64[us]', freq='D')
350
351 The ``freq`` parameter specifies the frequency of the TimedeltaIndex.
352 Only fixed frequencies can be passed, non-fixed frequencies such as
353 'M' (month end) will raise.
354
355 >>> pd.timedelta_range(start="1 day", end="2 days", freq="6h")
356 TimedeltaIndex(['1 days 00:00:00', '1 days 06:00:00', '1 days 12:00:00',
357 '1 days 18:00:00', '2 days 00:00:00'],
358 dtype='timedelta64[us]', freq='6h')
359
360 Specify ``start``, ``end``, and ``periods``; the frequency is generated
361 automatically (linearly spaced).
362
363 >>> pd.timedelta_range(start="1 day", end="5 days", periods=4)
364 TimedeltaIndex(['1 days 00:00:00', '2 days 08:00:00', '3 days 16:00:00',
365 '5 days 00:00:00'],
366 dtype='timedelta64[us]', freq=None)
367
368 **Specify a unit**
369
370 >>> pd.timedelta_range("1 Day", periods=3, freq="100000D", unit="s")
371 TimedeltaIndex(['1 days', '100001 days', '200001 days'],
372 dtype='timedelta64[s]', freq='100000D')
373 """
374 if freq is None and com.any_none(periods, start, end):
375 freq = "D"
376 freq = to_offset(freq)
377
378 if com.count_not_none(start, end, periods, freq) != 3:
379 # This check needs to come before the `unit = start.unit` line below
380 raise ValueError(
381 "Of the four parameters: start, end, periods, "
382 "and freq, exactly three must be specified"
383 )
384
385 if unit is None:
386 # Infer the unit based on the inputs
387
388 if start is not None and end is not None:
389 start = Timedelta(start)
390 end = Timedelta(end)
391 start = cast(Timedelta, start)
392 end = cast(Timedelta, end)
393 if abbrev_to_npy_unit(start.unit) > abbrev_to_npy_unit(end.unit):
394 unit = cast("TimeUnit", start.unit)
395 else:
396 unit = cast("TimeUnit", end.unit)
397 elif start is not None:
398 start = Timedelta(start)
399 start = cast(Timedelta, start)
400 unit = cast("TimeUnit", start.unit)
401 else:
402 end = Timedelta(end)
403 end = cast(Timedelta, end)
404 unit = cast("TimeUnit", end.unit)
405
406 # Last we need to watch out for cases where the 'freq' implies a higher
407 # unit than either start or end
408 if freq is not None:
409 freq = cast("Tick | Day", freq)
410 creso = abbrev_to_npy_unit(unit)
411 if freq._creso > creso: # pyright: ignore[reportAttributeAccessIssue]
412 unit = cast("TimeUnit", freq.base.freqstr)
413
414 tdarr = TimedeltaArray._generate_range(
415 start, end, periods, freq, closed=closed, unit=unit
416 )
417 return TimedeltaIndex._simple_new(tdarr, name=name)