1from __future__ import annotations
2
3from datetime import timedelta
4import operator
5import textwrap
6from typing import (
7 TYPE_CHECKING,
8 Self,
9 cast,
10)
11
12import numpy as np
13
14from pandas._libs import (
15 lib,
16 tslibs,
17)
18from pandas._libs.tslibs import (
19 Day,
20 NaT,
21 NaTType,
22 Resolution,
23 Tick,
24 Timedelta,
25 astype_overflowsafe,
26 get_resolution,
27 get_supported_dtype,
28 iNaT,
29 is_supported_dtype,
30 mul_overflowsafe,
31 periods_per_second,
32)
33from pandas._libs.tslibs.conversion import cast_from_unit_vectorized
34from pandas._libs.tslibs.fields import (
35 get_timedelta_days,
36 get_timedelta_field,
37)
38from pandas._libs.tslibs.timedeltas import (
39 array_to_timedelta64,
40 contains_str,
41 floordiv_object_array,
42 ints_to_pytimedelta,
43 parse_timedelta_unit,
44 truediv_object_array,
45)
46from pandas.compat.numpy import function as nv
47from pandas.errors import (
48 OutOfBoundsDatetime,
49 OutOfBoundsTimedelta,
50)
51from pandas.util._decorators import set_module
52from pandas.util._validators import validate_endpoints
53
54from pandas.core.dtypes.common import (
55 TD64NS_DTYPE,
56 is_float_dtype,
57 is_integer_dtype,
58 is_object_dtype,
59 is_scalar,
60 is_string_dtype,
61 pandas_dtype,
62)
63from pandas.core.dtypes.dtypes import (
64 ArrowDtype,
65 BaseMaskedDtype,
66 ExtensionDtype,
67)
68from pandas.core.dtypes.missing import isna
69
70from pandas.core import (
71 nanops,
72 roperator,
73)
74from pandas.core.array_algos import datetimelike_accumulations
75from pandas.core.arrays import datetimelike as dtl
76from pandas.core.arrays._ranges import generate_regular_range
77from pandas.core.arrays.integer import IntegerArray
78import pandas.core.common as com
79from pandas.core.construction import extract_array
80from pandas.core.ops.common import unpack_zerodim_and_defer
81
82if TYPE_CHECKING:
83 from collections.abc import Callable
84
85 from pandas._typing import (
86 AxisInt,
87 DateTimeErrorChoices,
88 DtypeObj,
89 NpDtype,
90 TimeUnit,
91 npt,
92 )
93
94 from pandas import DataFrame
95
96
97def _exact_if_integral(other):
98 """
99 Return the exact int equivalent of an integral float, otherwise `other`.
100
101 GH#66551 float64 carries only a 53-bit mantissa, so applying a float operand
102 in floating point rounds results that int64 represents exactly. An integral
103 float has an exact int equivalent, so the int path can be used instead.
104 inf and nan are not integral and so stay in float64.
105
106 Zero is left alone: it needs no exactness fix, and routing it to the int
107 path would turn division by 0.0 from NaT into a raise.
108 """
109 if lib.is_float(other) and other != 0 and other.is_integer():
110 return int(other)
111 return other
112
113
114def _field_accessor(name: str, alias: str, docstring: str):
115 def f(self) -> np.ndarray:
116 values = self.asi8
117 if alias == "days":
118 result = get_timedelta_days(values, reso=self._creso)
119 else:
120 # error: Incompatible types in assignment (
121 # expression has type "ndarray[Any, dtype[signedinteger[_32Bit]]]",
122 # variable has type "ndarray[Any, dtype[signedinteger[_64Bit]]]
123 result = get_timedelta_field(values, alias, reso=self._creso) # type: ignore[assignment]
124 if self._hasna:
125 result = self._maybe_mask_results(
126 result, fill_value=None, convert="float64"
127 )
128
129 return result
130
131 f.__name__ = name
132 f.__doc__ = f"\n{docstring}\n"
133 return property(f)
134
135
136@set_module("pandas.arrays")
137class TimedeltaArray(dtl.TimelikeOps):
138 """
139 Pandas ExtensionArray for timedelta data.
140
141 .. warning::
142
143 TimedeltaArray is currently experimental, and its API may change
144 without warning. In particular, :attr:`TimedeltaArray.dtype` is
145 expected to change to be an instance of an ``ExtensionDtype``
146 subclass.
147
148 Parameters
149 ----------
150 data : array-like
151 The timedelta data.
152 dtype : numpy.dtype
153 Valid ``numpy`` dtypes are ``timedelta64[ns]``, ``timedelta64[us]``,
154 ``timedelta64[ms]``, and ``timedelta64[s]``.
155 freq : Offset, optional
156 Frequency of the data.
157 copy : bool, default False
158 Whether to copy the underlying array of data.
159
160 Attributes
161 ----------
162 None
163
164 Methods
165 -------
166 None
167
168 See Also
169 --------
170 Timedelta : Represents a duration, the difference between two dates or times.
171 TimedeltaIndex : Immutable Index of timedelta64 data.
172 to_timedelta : Convert argument to timedelta.
173
174 Examples
175 --------
176 >>> pd.arrays.TimedeltaArray._from_sequence(pd.TimedeltaIndex(["1h", "2h"]))
177 <TimedeltaArray>
178 ['0 days 01:00:00', '0 days 02:00:00']
179 Length: 2, dtype: timedelta64[us]
180 """
181
182 _typ = "timedeltaarray"
183 _recognized_scalars = (timedelta, np.timedelta64, Tick)
184 _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: lib.is_np_dtype(x, "m")
185
186 @property
187 def _internal_fill_value(self) -> np.timedelta64:
188 return np.timedelta64("NaT", self.unit)
189
190 @property
191 def _scalar_type(self) -> type[Timedelta]:
192 return Timedelta
193
194 __array_priority__ = 1000
195 # define my properties & methods for delegation
196 _other_ops: list[str] = []
197 _bool_ops: list[str] = []
198 _field_ops: list[str] = ["days", "seconds", "microseconds", "nanoseconds"]
199 _datetimelike_ops: list[str] = _field_ops + _bool_ops + ["unit"]
200 _datetimelike_methods: list[str] = [
201 "to_pytimedelta",
202 "total_seconds",
203 "round",
204 "floor",
205 "ceil",
206 "as_unit",
207 ]
208
209 # Note: ndim must be defined to ensure NaT.__richcmp__(TimedeltaArray)
210 # operates pointwise.
211
212 def _box_func(self, x: np.timedelta64) -> Timedelta | NaTType:
213 y = x.view("i8")
214 if y == NaT._value:
215 return NaT
216 return Timedelta._from_value_and_reso(y, reso=self._creso)
217
218 @property
219 # error: Return type "dtype" of "dtype" incompatible with return type
220 # "ExtensionDtype" in supertype "ExtensionArray"
221 def dtype(self) -> np.dtype[np.timedelta64]: # type: ignore[override]
222 """
223 The dtype for the TimedeltaArray.
224
225 .. warning::
226
227 A future version of pandas will change dtype to be an instance
228 of a :class:`pandas.api.extensions.ExtensionDtype` subclass,
229 not a ``numpy.dtype``.
230
231 Returns
232 -------
233 numpy.dtype
234 """
235 return self._ndarray.dtype
236
237 @property # NB: override with cache_readonly in immutable subclasses
238 def _resolution_obj(self) -> Resolution:
239 return get_resolution(self.asi8, tz=None, reso=self._creso)
240
241 # ----------------------------------------------------------------
242 # Constructors
243
244 @classmethod
245 def _validate_dtype(cls, values, dtype):
246 # used in TimeLikeOps.__init__
247 dtype = _validate_td64_dtype(dtype)
248 _validate_td64_dtype(values.dtype)
249 if dtype != values.dtype:
250 raise ValueError("Values resolution does not match dtype.")
251 return dtype
252
253 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked"
254 @classmethod
255 def _simple_new( # type: ignore[override]
256 cls,
257 values: npt.NDArray[np.timedelta64],
258 dtype: np.dtype[np.timedelta64] = TD64NS_DTYPE,
259 ) -> Self:
260 # Require td64 dtype, not unit-less, matching values.dtype
261 assert lib.is_np_dtype(dtype, "m")
262 assert not tslibs.is_unitless(dtype)
263 assert isinstance(values, np.ndarray), type(values)
264 assert dtype == values.dtype
265
266 return super()._simple_new(values=values, dtype=dtype)
267
268 @classmethod
269 def _from_sequence(cls, data, *, dtype=None, copy: bool = False) -> Self:
270 if dtype:
271 dtype = _validate_td64_dtype(dtype)
272
273 data, copy = dtl.ensure_arraylike_for_datetimelike(
274 data, copy, cls_name="TimedeltaArray"
275 )
276
277 unit = None
278 if dtype is not None:
279 if data.dtype == object:
280 # GH#68639 the unit applies to the numeric entries, matching
281 # to_timedelta(data, unit=...), except that a str alongside
282 # them would make array_to_timedelta64 reject the unit
283 apply_unit = not contains_str(data)
284 else:
285 apply_unit = data.dtype.kind in "iuf"
286 if apply_unit:
287 unit = np.datetime_data(dtype)[0]
288
289 data = sequence_to_td64ns(data, copy=copy, unit=unit)
290
291 if dtype is not None:
292 data = astype_overflowsafe(data, dtype=dtype, copy=False)
293
294 return cls._simple_new(data, dtype=data.dtype)
295
296 @classmethod
297 def _generate_range(
298 cls, start, end, periods, freq, closed=None, *, unit: TimeUnit
299 ) -> Self:
300 periods = dtl.validate_periods(periods)
301 if freq is None and any(x is None for x in [periods, start, end]):
302 raise ValueError("Must provide freq argument if no data is supplied")
303
304 if com.count_not_none(start, end, periods, freq) != 3:
305 raise ValueError(
306 "Of the four parameters: start, end, periods, "
307 "and freq, exactly three must be specified"
308 )
309
310 if start is not None:
311 start = Timedelta(start).as_unit("ns")
312
313 if end is not None:
314 end = Timedelta(end).as_unit("ns")
315
316 if unit not in ["s", "ms", "us", "ns"]:
317 raise ValueError("'unit' must be one of 's', 'ms', 'us', 'ns'")
318
319 if start is not None and unit is not None:
320 start = start.as_unit(unit, round_ok=False)
321 if end is not None and unit is not None:
322 end = end.as_unit(unit, round_ok=False)
323
324 left_closed, right_closed = validate_endpoints(closed)
325
326 if freq is not None:
327 index = generate_regular_range(start, end, periods, freq, unit=unit)
328 else:
329 index = np.linspace(start._value, end._value, periods).astype("i8")
330
331 if not left_closed:
332 index = index[1:]
333 if not right_closed:
334 index = index[:-1]
335
336 td64values = index.view(f"m8[{unit}]")
337 return cls._simple_new(td64values, dtype=td64values.dtype)
338
339 # ----------------------------------------------------------------
340 # DatetimeLike Interface
341
342 def _unbox_scalar(self, value) -> np.timedelta64:
343 if not isinstance(value, self._scalar_type) and value is not NaT:
344 raise ValueError("'value' should be a Timedelta.")
345 self._check_compatible_with(value)
346 if value is NaT:
347 return np.timedelta64(value._value, self.unit)
348 else:
349 # error: Incompatible return value type (got "timedelta64[timedelta |
350 # int | None] | datetime64[date | int | None]",
351 # expected "timedelta64[timedelta | int | None]")
352 return value.as_unit(self.unit, round_ok=False).asm8 # type: ignore[return-value]
353
354 def _scalar_from_string(self, value) -> Timedelta | NaTType:
355 return Timedelta(value)
356
357 def _check_compatible_with(self, other) -> None:
358 # we don't have anything to validate.
359 pass
360
361 # ----------------------------------------------------------------
362 # Array-Like / EA-Interface Methods
363
364 def astype(self, dtype, copy: bool = True):
365 # We handle
366 # --> timedelta64[ns]
367 # --> timedelta64
368 # DatetimeLikeArrayMixin super call handles other cases
369 dtype = pandas_dtype(dtype)
370
371 if lib.is_np_dtype(dtype, "m"):
372 if dtype == self.dtype:
373 if copy:
374 return self.copy()
375 return self
376
377 if is_supported_dtype(dtype):
378 # unit conversion e.g. timedelta64[s]
379 res_values = astype_overflowsafe(self._ndarray, dtype, copy=False)
380 return type(self)._simple_new(res_values, dtype=res_values.dtype)
381 else:
382 raise ValueError(
383 f"Cannot convert from {self.dtype} to {dtype}. "
384 "Supported resolutions are 's', 'ms', 'us', 'ns'"
385 )
386
387 return dtl.DatetimeLikeArrayMixin.astype(self, dtype, copy=copy)
388
389 def _iter_convert_chunk(self, data: np.ndarray) -> np.ndarray:
390 return ints_to_pytimedelta(data, box=True)
391
392 # ----------------------------------------------------------------
393 # Reductions
394
395 def sum(
396 self,
397 *,
398 axis: AxisInt | None = None,
399 dtype: NpDtype | None = None,
400 out=None,
401 keepdims: bool = False,
402 initial=None,
403 skipna: bool = True,
404 min_count: int = 0,
405 ):
406 nv.validate_sum(
407 (), {"dtype": dtype, "out": out, "keepdims": keepdims, "initial": initial}
408 )
409
410 result = nanops.nansum(
411 self._ndarray, axis=axis, skipna=skipna, min_count=min_count
412 )
413 return self._wrap_reduction_result(axis, result)
414
415 def std(
416 self,
417 *,
418 axis: AxisInt | None = None,
419 dtype: NpDtype | None = None,
420 out=None,
421 ddof: int = 1,
422 keepdims: bool = False,
423 skipna: bool = True,
424 ):
425 nv.validate_stat_ddof_func(
426 (), {"dtype": dtype, "out": out, "keepdims": keepdims}, fname="std"
427 )
428
429 result = nanops.nanstd(self._ndarray, axis=axis, skipna=skipna, ddof=ddof)
430 return self._wrap_reduction_result(axis, result)
431
432 # ----------------------------------------------------------------
433 # Accumulations
434
435 def _accumulate(self, name: str, *, skipna: bool = True, **kwargs):
436 if name == "cumsum":
437 op = getattr(datetimelike_accumulations, name)
438 result = op(self._ndarray.copy(), skipna=skipna, **kwargs)
439
440 return type(self)._simple_new(result, dtype=self.dtype)
441 elif name == "cumprod":
442 raise TypeError("cumprod not supported for Timedelta.")
443
444 else:
445 return super()._accumulate(name, skipna=skipna, **kwargs)
446
447 # ----------------------------------------------------------------
448 # Rendering Methods
449
450 def _formatter(self, boxed: bool = False):
451 from pandas.io.formats.format import get_format_timedelta64
452
453 return get_format_timedelta64(self, box=True)
454
455 def _format_native_types(
456 self, *, na_rep: str | float = "NaT", date_format=None, **kwargs
457 ) -> npt.NDArray[np.object_]:
458 from pandas.io.formats.format import get_format_timedelta64
459
460 # Relies on TimeDelta._repr_base
461 formatter = get_format_timedelta64(self, na_rep)
462 # equiv: np.array([formatter(x) for x in self._ndarray])
463 # but independent of dimension
464 return np.frompyfunc(formatter, 1, 1)(self._ndarray)
465
466 # ----------------------------------------------------------------
467 # Arithmetic Methods
468
469 def _add_offset(self, other):
470 assert not isinstance(other, (Tick, Day))
471 raise TypeError(
472 f"cannot add the type {type(other).__name__} to a {type(self).__name__}"
473 )
474
475 def _mul_float_overflowsafe(
476 self, other: float | np.floating | npt.NDArray[np.floating]
477 ) -> Self:
478 # GH#43178: detect float products that would silently saturate to
479 # int64.max on the int64 cast below
480 i8 = self.asi8
481 self_mask = i8 == iNaT
482 if self_mask.any():
483 # zero out NaT positions so they don't trigger the bounds check
484 i8 = np.where(self_mask, 0, i8)
485 f_result = i8 * other
486 nan_mask = np.isnan(f_result)
487 non_nan = f_result[~nan_mask]
488 # Compare against 2**63, not i8max: i8max (2**63 - 1) rounds up to
489 # 2**63 in float64, so a product landing exactly on 2**63 would slip
490 # past a ``> i8max`` check and saturate on the cast. Also catches +/-inf.
491 if non_nan.size and np.max(np.abs(non_nan), initial=0.0) >= 2.0**63:
492 raise OutOfBoundsTimedelta("Overflow in timedelta multiplication")
493 # NaN-to-int cast is platform-dependent; substitute 0 then re-mask as NaT
494 if nan_mask.any():
495 f_result = np.where(nan_mask, 0.0, f_result)
496 i8_result = f_result.astype("i8")
497 nat_out = self_mask | nan_mask
498 if nat_out.any():
499 i8_result[nat_out] = iNaT
500 result = i8_result.view(self._ndarray.dtype)
501 return type(self)._simple_new(result, dtype=result.dtype)
502
503 def _mul_int_overflowsafe(self, i8_other: npt.NDArray[np.int64]) -> Self:
504 # GH#43178: mul_overflowsafe raises the low-level OverflowError; surface
505 # it as OutOfBoundsTimedelta to match pandas' other td64 overflow paths.
506 try:
507 i8_result = mul_overflowsafe(self.asi8, i8_other)
508 except OverflowError as err:
509 raise OutOfBoundsTimedelta("Overflow in int64 multiplication") from err
510 result = i8_result.view(self._ndarray.dtype)
511 return type(self)._simple_new(result, dtype=result.dtype)
512
513 @unpack_zerodim_and_defer("__mul__")
514 def __mul__(self, other) -> Self:
515 if is_scalar(other):
516 if lib.is_bool(other):
517 raise TypeError(
518 f"Cannot multiply '{self.dtype}' by bool, explicitly cast to "
519 "integers instead"
520 )
521 other = _exact_if_integral(other)
522 if lib.is_integer(other):
523 # GH#43178: detect int64 overflow rather than silently wrapping
524 # in the i8 cast below (e.g. a multiplier outside int64 bounds).
525 # TODO(numpy>=2.5): numpy detects this natively (numpy GH-31378)
526 # but raises OverflowError; once the numpy floor is >= 2.5, drop
527 # mul_overflowsafe and re-wrap numpy's error as
528 # OutOfBoundsTimedelta. The non-integral float path isn't
529 # covered and stays.
530 other = int(other)
531 if other > lib.i8max or other < -lib.i8max - 1:
532 raise OutOfBoundsTimedelta("Overflow in int64 multiplication")
533 i8_vals = self.asi8
534 if other != 0 and i8_vals.size:
535 # The extreme elements bound all products, so checking them
536 # with exact Python-int arithmetic lets the common
537 # no-overflow case use a vectorized multiply. NaT
538 # (int64.min) always trips the bound, falling through to
539 # the NaT-aware cython loop.
540 low_prod = int(i8_vals.min()) * other
541 high_prod = int(i8_vals.max()) * other
542 if max(abs(low_prod), abs(high_prod)) <= lib.i8max:
543 result = (i8_vals * other).view(self._ndarray.dtype)
544 return type(self)._simple_new(result, dtype=result.dtype)
545 return self._mul_int_overflowsafe(np.asarray(other, dtype="i8"))
546 if lib.is_float(other):
547 return self._mul_float_overflowsafe(other)
548 # numpy will raise TypeError for non-numeric scalar
549 result = self._ndarray * other
550 if result.dtype.kind != "m":
551 # numpy >= 2.1 may not raise a TypeError
552 # and seems to dispatch to others.__rmul__?
553 raise TypeError(f"Cannot multiply with {type(other).__name__}")
554 return type(self)._simple_new(result, dtype=result.dtype)
555
556 if not hasattr(other, "dtype"):
557 # list, tuple
558 other = np.array(other)
559
560 if other.dtype.kind == "b":
561 # GH#58054
562 raise TypeError(
563 f"Cannot multiply '{self.dtype}' by bool, explicitly cast to "
564 "integers instead"
565 )
566 if isinstance(other.dtype, (ArrowDtype, BaseMaskedDtype)):
567 # GH#58054
568 return NotImplemented
569
570 if len(other) != len(self) and not lib.is_np_dtype(other.dtype, "m"):
571 # Exclude timedelta64 here so we correctly raise TypeError
572 # for that instead of ValueError
573 raise ValueError("Cannot multiply with unequal lengths")
574
575 if is_object_dtype(other.dtype):
576 # this multiplication will succeed only if all elements of other
577 # are int or float scalars, so we will end up with
578 # timedelta64[ns]-dtyped result
579 arr = self._ndarray
580 obj_result = np.array([arr[n] * other[n] for n in range(len(self))])
581 return type(self)._simple_new(obj_result, dtype=obj_result.dtype)
582
583 if other.dtype.kind in "iu":
584 # GH#43178: detect int64 overflow rather than silently wrapping.
585 # Cast to int64 first: an unsigned multiplier above int64.max wraps
586 # to negative, which we detect by sign. We check the sign rather
587 # than ``other > i8max`` because comparing a broadcast unsigned
588 # array to a Python int segfaults on numpy < 2.2 (hit via the
589 # DataFrame blockwise path).
590 i8_other = other.astype("i8", copy=False)
591 if other.dtype.kind == "u" and (i8_other < 0).any():
592 raise OutOfBoundsTimedelta("Overflow in int64 multiplication")
593 return self._mul_int_overflowsafe(i8_other)
594
595 if other.dtype.kind == "f":
596 return self._mul_float_overflowsafe(other)
597
598 # numpy will raise TypeError for non-numeric dtype
599 result = self._ndarray * other
600 if result.dtype.kind != "m":
601 # numpy >= 2.1 may not raise a TypeError
602 # and seems to dispatch to others.__rmul__?
603 raise TypeError(f"Cannot multiply with {type(other).__name__}")
604 return type(self)._simple_new(result, dtype=result.dtype)
605
606 __rmul__ = __mul__
607
608 def _check_float_div_overflow(
609 self, other: float | np.floating | npt.NDArray[np.floating]
610 ) -> None:
611 # GH#43178: numpy's timedelta64-by-float division silently saturates
612 # when the quotient exceeds int64 bounds; detect that on the float64
613 # quotient and raise instead. NaT dividends and zero/NaN divisors are
614 # excluded: numpy returns NaT for those. A float64 quotient inside
615 # (-2**63, 2**63) is at least an ulp (1024) away from the boundary,
616 # so the floor in __floordiv__ cannot push it out of bounds.
617 i8 = self.asi8
618 other_arr = np.asarray(other)
619 if other_arr.ndim == 0 and i8.size:
620 divisor = other_arr.item()
621 if divisor == 0 or np.isnan(divisor):
622 # numpy returns all-NaT; nothing to check
623 return
624 # The extreme elements bound all quotients, so most cases resolve
625 # without the full per-element check below. A NaT (int64.min)
626 # dividend can only false-trip this bound, never pass an
627 # overflowing quotient.
628 low_quot = i8.min() / divisor
629 high_quot = i8.max() / divisor
630 if max(abs(low_quot), abs(high_quot)) < 2.0**63:
631 return
632 with np.errstate(divide="ignore", invalid="ignore"):
633 f_quot = i8 / other_arr
634 exclude_mask = (i8 == iNaT) | np.isnan(f_quot) | (other_arr == 0)
635 if np.max(np.abs(f_quot), initial=0.0, where=~exclude_mask) >= 2.0**63:
636 raise OutOfBoundsTimedelta("Overflow in timedelta division")
637
638 def _scalar_divlike_op(self, other, op):
639 """
640 Shared logic for __truediv__, __rtruediv__, __floordiv__, __rfloordiv__
641 with scalar 'other'.
642 """
643 if isinstance(other, self._recognized_scalars):
644 other = Timedelta(other)
645 # mypy assumes that __new__ returns an instance of the class
646 # github.com/python/mypy/issues/1020
647 if cast("Timedelta | NaTType", other) is NaT:
648 # specifically timedelta64-NaT
649 res = np.empty(self.shape, dtype=np.float64)
650 res.fill(np.nan)
651 return res
652
653 # otherwise, dispatch to Timedelta implementation
654 return op(self._ndarray, other)
655
656 else:
657 # caller is responsible for checking lib.is_scalar(other)
658 # assume other is numeric, otherwise numpy will raise
659
660 if op in [roperator.rtruediv, roperator.rfloordiv]:
661 raise TypeError(
662 f"Cannot divide {type(other).__name__} by {type(self).__name__}"
663 )
664
665 other = _exact_if_integral(other)
666 if lib.is_float(other):
667 # GH#43178: raise instead of silently saturating on overflow
668 self._check_float_div_overflow(other)
669 result = op(self._ndarray, other)
670 return type(self)._simple_new(result, dtype=result.dtype)
671
672 def _cast_divlike_op(self, other):
673 if not hasattr(other, "dtype"):
674 # e.g. list, tuple
675 other = np.array(other)
676
677 if len(other) != len(self):
678 raise ValueError("Cannot divide vectors with unequal lengths")
679 return other
680
681 def _vector_divlike_op(self, other, op) -> np.ndarray | Self:
682 """
683 Shared logic for __truediv__, __floordiv__, and their reversed versions
684 with timedelta64-dtype ndarray other.
685 """
686 other_arr = np.asarray(other)
687 if other_arr.dtype.kind == "f" and op in [operator.truediv, operator.floordiv]:
688 # GH#43178: raise instead of silently saturating on overflow
689 self._check_float_div_overflow(other_arr)
690
691 # Let numpy handle it
692 result = op(self._ndarray, other_arr)
693
694 if (is_integer_dtype(other.dtype) or is_float_dtype(other.dtype)) and op in [
695 operator.truediv,
696 operator.floordiv,
697 ]:
698 return type(self)._simple_new(result, dtype=result.dtype)
699
700 if op in [operator.floordiv, roperator.rfloordiv]:
701 mask = self.isna() | isna(other)
702 if mask.any():
703 result = result.astype(np.float64)
704 np.putmask(result, mask, np.nan)
705
706 return result
707
708 @unpack_zerodim_and_defer("__truediv__")
709 def __truediv__(self, other):
710 # timedelta / X is well-defined for timedelta-like or numeric X
711 op = operator.truediv
712 if is_scalar(other):
713 return self._scalar_divlike_op(other, op)
714
715 other = self._cast_divlike_op(other)
716 if (
717 lib.is_np_dtype(other.dtype, "m")
718 or is_integer_dtype(other.dtype)
719 or is_float_dtype(other.dtype)
720 ):
721 return self._vector_divlike_op(other, op)
722
723 if is_object_dtype(other.dtype):
724 other = np.asarray(other)
725 if self.ndim > 1:
726 res_cols = [
727 left / right for left, right in zip(self, other, strict=True)
728 ]
729 res_cols2 = [x.reshape(1, -1) for x in res_cols]
730 result = np.concatenate(res_cols2, axis=0)
731 else:
732 result = truediv_object_array(self._ndarray, other)
733
734 return result
735
736 else:
737 return NotImplemented
738
739 @unpack_zerodim_and_defer("__rtruediv__")
740 def __rtruediv__(self, other):
741 # X / timedelta is defined only for timedelta-like X
742 op = roperator.rtruediv
743 if is_scalar(other):
744 return self._scalar_divlike_op(other, op)
745
746 other = self._cast_divlike_op(other)
747 if lib.is_np_dtype(other.dtype, "m"):
748 return self._vector_divlike_op(other, op)
749
750 elif is_object_dtype(other.dtype):
751 # Note: unlike in __truediv__, we do not _need_ to do type
752 # inference on the result. It does not raise, a numeric array
753 # is returned. GH#23829
754 result_list = [other[n] / self[n] for n in range(len(self))]
755 return np.array(result_list)
756
757 else:
758 return NotImplemented
759
760 @unpack_zerodim_and_defer("__floordiv__")
761 def __floordiv__(self, other):
762 op = operator.floordiv
763 if is_scalar(other):
764 return self._scalar_divlike_op(other, op)
765
766 other = self._cast_divlike_op(other)
767 if (
768 lib.is_np_dtype(other.dtype, "m")
769 or is_integer_dtype(other.dtype)
770 or is_float_dtype(other.dtype)
771 ):
772 return self._vector_divlike_op(other, op)
773
774 elif is_object_dtype(other.dtype):
775 other = np.asarray(other)
776 if self.ndim > 1:
777 res_cols = [
778 left // right for left, right in zip(self, other, strict=True)
779 ]
780 res_cols2 = [x.reshape(1, -1) for x in res_cols]
781 result = np.concatenate(res_cols2, axis=0)
782 else:
783 result = floordiv_object_array(self._ndarray, other)
784
785 assert result.dtype == object
786 return result
787
788 else:
789 return NotImplemented
790
791 @unpack_zerodim_and_defer("__rfloordiv__")
792 def __rfloordiv__(self, other):
793 op = roperator.rfloordiv
794 if is_scalar(other):
795 return self._scalar_divlike_op(other, op)
796
797 other = self._cast_divlike_op(other)
798 if lib.is_np_dtype(other.dtype, "m"):
799 return self._vector_divlike_op(other, op)
800
801 elif is_object_dtype(other.dtype):
802 result_list = [other[n] // self[n] for n in range(len(self))]
803 result = np.array(result_list)
804 return result
805
806 else:
807 return NotImplemented
808
809 @unpack_zerodim_and_defer("__mod__")
810 def __mod__(self, other):
811 # Note: This is a naive implementation, can likely be optimized
812 if isinstance(other, self._recognized_scalars):
813 other = Timedelta(other)
814 return self - (self // other) * other
815
816 @unpack_zerodim_and_defer("__rmod__")
817 def __rmod__(self, other):
818 # Note: This is a naive implementation, can likely be optimized
819 if isinstance(other, self._recognized_scalars):
820 other = Timedelta(other)
821 return other - (other // self) * self
822
823 @unpack_zerodim_and_defer("__divmod__")
824 def __divmod__(self, other):
825 # Note: This is a naive implementation, can likely be optimized
826 if isinstance(other, self._recognized_scalars):
827 other = Timedelta(other)
828
829 res1 = self // other
830 res2 = self - res1 * other
831 return res1, res2
832
833 @unpack_zerodim_and_defer("__rdivmod__")
834 def __rdivmod__(self, other):
835 # Note: This is a naive implementation, can likely be optimized
836 if isinstance(other, self._recognized_scalars):
837 other = Timedelta(other)
838
839 res1 = other // self
840 res2 = other - res1 * self
841 return res1, res2
842
843 def __neg__(self) -> TimedeltaArray:
844 return type(self)._simple_new(-self._ndarray, dtype=self.dtype)
845
846 def __pos__(self) -> TimedeltaArray:
847 return type(self)._simple_new(self._ndarray.copy(), dtype=self.dtype)
848
849 def __abs__(self) -> TimedeltaArray:
850 # Note: freq is not preserved
851 return type(self)._simple_new(np.abs(self._ndarray), dtype=self.dtype)
852
853 # ----------------------------------------------------------------
854 # Conversion Methods - Vectorized analogues of Timedelta methods
855
856 def total_seconds(self) -> npt.NDArray[np.float64]:
857 """
858 Return total duration of each element expressed in seconds.
859
860 This method is available directly on TimedeltaArray, TimedeltaIndex
861 and on Series containing timedelta values under the ``.dt`` namespace.
862
863 Returns
864 -------
865 ndarray, Index or Series
866 When the calling object is a TimedeltaArray, the return type
867 is ndarray. When the calling object is a TimedeltaIndex,
868 the return type is an Index with a float64 dtype. When the calling object
869 is a Series, the return type is Series of type `float64` whose
870 index is the same as the original.
871
872 See Also
873 --------
874 datetime.timedelta.total_seconds : Standard library version
875 of this method.
876 TimedeltaIndex.components : Return a DataFrame with components of
877 each Timedelta.
878
879 Examples
880 --------
881 **Series**
882
883 >>> s = pd.Series(pd.to_timedelta(np.arange(5), unit="D"))
884 >>> s
885 0 0 days
886 1 1 days
887 2 2 days
888 3 3 days
889 4 4 days
890 dtype: timedelta64[us]
891
892 >>> s.dt.total_seconds()
893 0 0.0
894 1 86400.0
895 2 172800.0
896 3 259200.0
897 4 345600.0
898 dtype: float64
899
900 **TimedeltaIndex**
901
902 >>> idx = pd.to_timedelta(np.arange(5), unit="D")
903 >>> idx
904 TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
905 dtype='timedelta64[us]', freq=None)
906
907 >>> idx.total_seconds()
908 Index([0.0, 86400.0, 172800.0, 259200.0, 345600.0], dtype='float64')
909 """
910 pps = periods_per_second(self._creso)
911 asi8 = self.asi8
912 # Divide the integer-second part and the sub-second residual
913 # separately, mirroring the same divmod split in the scalar
914 # `_Timedelta.total_seconds`, so the result matches it bit-for-bit
915 # (on IEEE-strict platforms; 32-bit x87 excess precision can differ
916 # in the last ulp). A single asi8 / pps division rounds at the tick
917 # magnitude and loses precision for values above 2**53.
918 floor_s, residual = np.divmod(asi8, pps)
919 result = floor_s + residual / pps
920 if (asi8 > 2**53).any() or (asi8 < -(2**53)).any():
921 # Only above 2**53 ticks can the float result round onto an
922 # integer-second boundary; the threshold is unit-independent
923 # because a collapse needs ulp(floor_s) > 2 / pps, i.e.
924 # |asi8| ~ floor_s * pps > 2**53. A sub-second residual puts the
925 # true value strictly inside (floor, floor + 1), so nudge off
926 # either boundary, otherwise bisect-style lookups (e.g. dateutil
927 # DST, GH#31043) misclassify the timestamp as on a transition.
928 # This mirrors the scalar guard in `_Timedelta.total_seconds`,
929 # including its bound: beyond 2**52 seconds float64s are spaced
930 # >= 1 second apart, so every representable value sits on a
931 # boundary and nudging would only add error.
932 fixable = (residual != 0) & (np.abs(floor_s) < 2**52)
933 floor_f = floor_s.astype(np.float64)
934 on_floor = fixable & (result == floor_f)
935 on_ceil = fixable & (result == floor_f + 1.0)
936 if on_floor.any():
937 result = np.where(on_floor, np.nextafter(result, result + 1.0), result)
938 if on_ceil.any():
939 result = np.where(on_ceil, np.nextafter(result, result - 1.0), result)
940 return self._maybe_mask_results(result, fill_value=None)
941
942 def to_pytimedelta(self) -> npt.NDArray[np.object_]:
943 """
944 Return an ndarray of datetime.timedelta objects.
945
946 Each element of the :class:`TimedeltaIndex` is converted to the
947 corresponding native Python :class:`datetime.timedelta` object.
948
949 Returns
950 -------
951 numpy.ndarray
952 A NumPy ``timedelta64`` object representing the same duration as the
953 original pandas ``Timedelta`` object. The precision of the resulting
954 object is in nanoseconds, which is the default
955 time resolution used by pandas for ``Timedelta`` objects, ensuring
956 high precision for time-based calculations.
957
958 See Also
959 --------
960 to_timedelta : Convert argument to timedelta format.
961 Timedelta : Represents a duration between two dates or times.
962 DatetimeIndex: Index of datetime64 data.
963 Timedelta.components : Return a components namedtuple-like
964 of a single timedelta.
965
966 Examples
967 --------
968 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
969 >>> tdelta_idx
970 TimedeltaIndex(['1 days', '2 days', '3 days'],
971 dtype='timedelta64[us]', freq=None)
972 >>> tdelta_idx.to_pytimedelta()
973 array([datetime.timedelta(days=1), datetime.timedelta(days=2),
974 datetime.timedelta(days=3)], dtype=object)
975
976 >>> tidx = pd.TimedeltaIndex(data=["1 days 02:30:45", "3 days 04:15:10"])
977 >>> tidx
978 TimedeltaIndex(['1 days 02:30:45', '3 days 04:15:10'],
979 dtype='timedelta64[us]', freq=None)
980 >>> tidx.to_pytimedelta()
981 array([datetime.timedelta(days=1, seconds=9045),
982 datetime.timedelta(days=3, seconds=15310)], dtype=object)
983 """
984 return ints_to_pytimedelta(self._ndarray)
985
986 days_docstring = textwrap.dedent("""Number of days for each element.
987
988 This attribute returns the number of whole days in each timedelta value.
989 It represents the days component of the duration, not the total duration
990 expressed in days.
991
992 See Also
993 --------
994 Series.dt.seconds : Return number of seconds for each element.
995 Series.dt.microseconds : Return number of microseconds for each element.
996 Series.dt.nanoseconds : Return number of nanoseconds for each element.
997
998 Examples
999 --------
1000 For Series:
1001
1002 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='D'))
1003 >>> ser
1004 0 1 days
1005 1 2 days
1006 2 3 days
1007 dtype: timedelta64[us]
1008 >>> ser.dt.days
1009 0 1
1010 1 2
1011 2 3
1012 dtype: int64
1013
1014 For TimedeltaIndex:
1015
1016 >>> tdelta_idx = pd.to_timedelta(["0 days", "10 days", "20 days"])
1017 >>> tdelta_idx
1018 TimedeltaIndex(['0 days', '10 days', '20 days'],
1019 dtype='timedelta64[us]', freq=None)
1020 >>> tdelta_idx.days
1021 Index([0, 10, 20], dtype='int64')""")
1022 days = _field_accessor("days", "days", days_docstring)
1023
1024 seconds_docstring = textwrap.dedent(
1025 """Number of seconds (>= 0 and less than 1 day) for each element.
1026
1027 This attribute returns the seconds component of each timedelta value,
1028 which is the number of seconds remaining after subtracting whole days.
1029 Values range from 0 to 86399.
1030
1031 See Also
1032 --------
1033 Series.dt.seconds : Return number of seconds for each element.
1034 Series.dt.nanoseconds : Return number of nanoseconds for each element.
1035
1036 Examples
1037 --------
1038 For Series:
1039
1040 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='s'))
1041 >>> ser
1042 0 0 days 00:00:01
1043 1 0 days 00:00:02
1044 2 0 days 00:00:03
1045 dtype: timedelta64[us]
1046 >>> ser.dt.seconds
1047 0 1
1048 1 2
1049 2 3
1050 dtype: int32
1051
1052 For TimedeltaIndex:
1053
1054 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='s')
1055 >>> tdelta_idx
1056 TimedeltaIndex(['0 days 00:00:01', '0 days 00:00:02', '0 days 00:00:03'],
1057 dtype='timedelta64[us]', freq=None)
1058 >>> tdelta_idx.seconds
1059 Index([1, 2, 3], dtype='int32')"""
1060 )
1061 seconds = _field_accessor(
1062 "seconds",
1063 "seconds",
1064 seconds_docstring,
1065 )
1066
1067 microseconds_docstring = textwrap.dedent(
1068 """Number of microseconds (>= 0 and less than 1 second) for each element.
1069
1070 This attribute returns the microseconds component of each timedelta value,
1071 which is the number of microseconds remaining after subtracting whole
1072 seconds. Values range from 0 to 999999.
1073
1074 See Also
1075 --------
1076 pd.Timedelta.microseconds : Number of microseconds (>= 0 and less than 1 second).
1077 pd.Timedelta.to_pytimedelta.microseconds : Number of microseconds (>= 0 and less
1078 than 1 second) of a datetime.timedelta.
1079
1080 Examples
1081 --------
1082 For Series:
1083
1084 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='us'))
1085 >>> ser
1086 0 0 days 00:00:00.000001
1087 1 0 days 00:00:00.000002
1088 2 0 days 00:00:00.000003
1089 dtype: timedelta64[us]
1090 >>> ser.dt.microseconds
1091 0 1
1092 1 2
1093 2 3
1094 dtype: int32
1095
1096 For TimedeltaIndex:
1097
1098 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='us')
1099 >>> tdelta_idx
1100 TimedeltaIndex(['0 days 00:00:00.000001', '0 days 00:00:00.000002',
1101 '0 days 00:00:00.000003'],
1102 dtype='timedelta64[us]', freq=None)
1103 >>> tdelta_idx.microseconds
1104 Index([1, 2, 3], dtype='int32')"""
1105 )
1106 microseconds = _field_accessor(
1107 "microseconds",
1108 "microseconds",
1109 microseconds_docstring,
1110 )
1111
1112 nanoseconds_docstring = textwrap.dedent(
1113 """Number of nanoseconds (>= 0 and less than 1 microsecond) for each element.
1114
1115 This attribute returns the nanoseconds component of each timedelta value,
1116 which is the number of nanoseconds remaining after subtracting whole
1117 microseconds. Values range from 0 to 999.
1118
1119 See Also
1120 --------
1121 Series.dt.seconds : Return number of seconds for each element.
1122 Series.dt.microseconds : Return number of microseconds for each element.
1123
1124 Examples
1125 --------
1126 For Series:
1127
1128 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='ns'))
1129 >>> ser
1130 0 0 days 00:00:00.000000001
1131 1 0 days 00:00:00.000000002
1132 2 0 days 00:00:00.000000003
1133 dtype: timedelta64[ns]
1134 >>> ser.dt.nanoseconds
1135 0 1
1136 1 2
1137 2 3
1138 dtype: int32
1139
1140 For TimedeltaIndex:
1141
1142 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='ns')
1143 >>> tdelta_idx
1144 TimedeltaIndex(['0 days 00:00:00.000000001', '0 days 00:00:00.000000002',
1145 '0 days 00:00:00.000000003'],
1146 dtype='timedelta64[ns]', freq=None)
1147 >>> tdelta_idx.nanoseconds
1148 Index([1, 2, 3], dtype='int32')"""
1149 )
1150 nanoseconds = _field_accessor(
1151 "nanoseconds",
1152 "nanoseconds",
1153 nanoseconds_docstring,
1154 )
1155
1156 @property
1157 def components(self) -> DataFrame:
1158 """
1159 Return a DataFrame of the individual resolution components of the Timedeltas.
1160
1161 The components (days, hours, minutes seconds, milliseconds, microseconds,
1162 nanoseconds) are returned as columns in a DataFrame.
1163
1164 Returns
1165 -------
1166 DataFrame
1167
1168 See Also
1169 --------
1170 TimedeltaIndex.total_seconds : Return total duration expressed in seconds.
1171 Timedelta.components : Return a components namedtuple-like of a single
1172 timedelta.
1173
1174 Examples
1175 --------
1176 >>> tdelta_idx = pd.to_timedelta(["1 day 3 min 2 us 42 ns"])
1177 >>> tdelta_idx
1178 TimedeltaIndex(['1 days 00:03:00.000002042'],
1179 dtype='timedelta64[ns]', freq=None)
1180 >>> tdelta_idx.components
1181 days hours minutes seconds milliseconds microseconds nanoseconds
1182 0 1 0 3 0 0 2 42
1183 """
1184 from pandas import DataFrame
1185
1186 columns = [
1187 "days",
1188 "hours",
1189 "minutes",
1190 "seconds",
1191 "milliseconds",
1192 "microseconds",
1193 "nanoseconds",
1194 ]
1195 hasnans = self._hasna
1196 if hasnans:
1197
1198 def f(x): # pyright: ignore[reportRedeclaration]
1199 if isna(x):
1200 return [np.nan] * len(columns)
1201 return x.components
1202
1203 else:
1204
1205 def f(x):
1206 return x.components
1207
1208 result = DataFrame([f(x) for x in self], columns=columns)
1209 if not hasnans:
1210 result = result.astype("int64")
1211 return result
1212
1213
1214# ---------------------------------------------------------------------
1215# Constructor Helpers
1216
1217
1218def sequence_to_td64ns(
1219 data,
1220 copy: bool = False,
1221 unit=None,
1222 errors: DateTimeErrorChoices = "raise",
1223) -> np.ndarray:
1224 """
1225 Parameters
1226 ----------
1227 data : list-like
1228 copy : bool, default False
1229 unit : str, optional
1230 The timedelta unit to treat integers as multiples of. For numeric
1231 data this defaults to ``'ns'``.
1232 Must be un-specified if the data contains a str and ``errors=="raise"``.
1233 errors : {"raise", "coerce", "ignore"}, default "raise"
1234 How to handle elements that cannot be converted to timedelta64[ns].
1235 See ``pandas.to_timedelta`` for details.
1236
1237 Returns
1238 -------
1239 converted : numpy.ndarray
1240 The sequence converted to a numpy array with dtype ``timedelta64[ns]``.
1241
1242 Raises
1243 ------
1244 ValueError : Data cannot be converted to timedelta64[ns].
1245
1246 Notes
1247 -----
1248 Unlike `pandas.to_timedelta`, if setting ``errors=ignore`` will not cause
1249 errors to be ignored; they are caught and subsequently ignored at a
1250 higher level.
1251 """
1252 assert unit not in ["Y", "y", "M"] # caller is responsible for checking
1253
1254 if unit is not None:
1255 unit = parse_timedelta_unit(unit)
1256
1257 data = extract_array(data, extract_numpy=True)
1258
1259 int_mask = None
1260 if isinstance(data, IntegerArray):
1261 # GH#66988 use the underlying int/uint ndarray + mask directly instead
1262 # of going through to_numpy() to convert to int64, which could overflow
1263 # for large uint64 data
1264 int_mask = data._mask if data._hasna else None
1265 data = data._data
1266 if int_mask is not None:
1267 # set to 0 to avoid OOB on masked values (setting iNaT only works for int64)
1268 # will get converted to NaT later
1269 data = data.copy()
1270 data[int_mask] = 0
1271
1272 data, copy = dtl.ensure_arraylike_for_datetimelike(
1273 data, copy, cls_name="TimedeltaArray"
1274 )
1275
1276 # Convert whatever we have into timedelta64[ns] dtype
1277 if data.dtype == object or is_string_dtype(data.dtype):
1278 # no need to make a copy, need to convert if string-dtyped
1279 data = _objects_to_td64ns(data, unit=unit, errors=errors)
1280 copy = False
1281
1282 elif is_integer_dtype(data.dtype):
1283 # treat as multiples of the given unit
1284 try:
1285 data, copy_made = _ints_to_td64ns(data, unit=unit)
1286 except OutOfBoundsTimedelta:
1287 if errors == "raise":
1288 raise
1289 data = data.astype(object)
1290 if int_mask is not None:
1291 data[int_mask] = None
1292 data = _objects_to_td64ns(data, unit=unit, errors=errors)
1293 copy_made = True
1294 else:
1295 if int_mask is not None:
1296 data[int_mask] = iNaT
1297 # copy was already made before calling ensure_arraylike_for_datetimelike
1298 copy_made = True
1299 copy = copy and not copy_made
1300
1301 elif is_float_dtype(data.dtype):
1302 # cast the unit, multiply base/frac separately
1303 # to avoid precision issues from float -> int
1304 if isinstance(data.dtype, ExtensionDtype):
1305 mask = data._mask
1306 data = data._data
1307 else:
1308 mask = np.isnan(data)
1309
1310 if unit is not None and unit != "ns":
1311 # if all non-NaN entries are round, treat these like ints
1312 # (which results in 'us' unit instead of 'ns')
1313 with np.errstate(invalid="ignore"):
1314 int_data = data.astype(np.int64)
1315 # On ARM, float-to-int64 overflow saturates to INT64_MAX
1316 # instead of wrapping, which makes the data == int_data
1317 # check pass incorrectly for OOB values like float(2**63).
1318 # Exclude values outside the int64 domain from the check.
1319 i64 = np.iinfo(np.int64)
1320 in_int64_range = (data >= np.float64(i64.min)) & (
1321 data < np.float64(i64.max)
1322 )
1323 all_round = (mask | (in_int64_range & (data == int_data))).all()
1324 if all_round:
1325 result = sequence_to_td64ns(
1326 int_data, copy=False, unit=unit, errors=errors
1327 )
1328 result[mask] = iNaT
1329 return result
1330
1331 data = data.astype(np.float64, copy=False)
1332 try:
1333 converted = cast_from_unit_vectorized(data, unit or "ns")
1334 except OutOfBoundsDatetime as err:
1335 if errors == "raise":
1336 raise OutOfBoundsTimedelta(*err.args) from err
1337 # GH#66823 fall back to the element-wise path, which honors
1338 # errors="coerce", like the integer branch above does.
1339 data = np.where(mask, np.nan, data)
1340 data = _objects_to_td64ns(data.astype(object), unit=unit, errors=errors)
1341 else:
1342 converted[mask] = iNaT
1343 data = converted.view("m8[ns]")
1344 copy = False
1345
1346 elif lib.is_np_dtype(data.dtype, "m"):
1347 if data.dtype.byteorder == ">":
1348 # GH#68342 supported units are otherwise stored as-is; the swap also
1349 # has to precede the cast, whose finer->coarser branch views i8 raw
1350 data = data.astype(data.dtype.newbyteorder("<"))
1351 copy = False
1352
1353 if not is_supported_dtype(data.dtype):
1354 # cast to closest supported unit, i.e. s or ns
1355 new_dtype = get_supported_dtype(data.dtype)
1356 data = astype_overflowsafe(data, dtype=new_dtype, copy=False)
1357 copy = False
1358
1359 else:
1360 # This includes datetime64-dtype, see GH#23539, GH#29794
1361 raise TypeError(f"dtype {data.dtype} cannot be converted to timedelta64[ns]")
1362
1363 if not copy:
1364 data = np.asarray(data)
1365 else:
1366 data = np.array(data, copy=copy)
1367
1368 assert data.dtype.kind == "m"
1369 assert data.dtype != "m8" # i.e. not unit-less
1370
1371 return data
1372
1373
1374def _ints_to_td64ns(data, unit: str = "ns") -> tuple[np.ndarray, bool]:
1375 """
1376 Convert an ndarray with integer-dtype to timedelta64[ns] dtype, treating
1377 the integers as multiples of the given timedelta unit.
1378
1379 Parameters
1380 ----------
1381 data : numpy.ndarray with integer-dtype
1382 unit : str, default "ns"
1383 The timedelta unit to treat integers as multiples of.
1384
1385 Returns
1386 -------
1387 numpy.ndarray : timedelta64[ns] array converted from data
1388 bool : whether a copy was made
1389 """
1390 copy_made = False
1391 unit = unit if unit is not None else "ns"
1392
1393 if data.dtype != np.int64:
1394 # GH#60677 unsigned integers > int64 max overflow silently
1395 # when cast to int64 (which timedelta64 is backed by)
1396 if data.dtype == np.dtype("uint64") and (data > np.iinfo(np.int64).max).any():
1397 raise OutOfBoundsTimedelta(f"Cannot convert input with unit '{unit}'")
1398 # converting to int64 makes a copy, so we can avoid
1399 # re-copying later
1400 data = data.astype(np.int64)
1401 copy_made = True
1402
1403 if unit != "ns":
1404 dtype_str = f"timedelta64[{unit}]"
1405 data = data.view(dtype_str)
1406
1407 if unit != "us":
1408 data = astype_overflowsafe(data, dtype=np.dtype("timedelta64[us]"))
1409
1410 # the astype conversion makes a copy, so we can avoid re-copying later
1411 copy_made = True
1412
1413 else:
1414 data = data.view("timedelta64[ns]")
1415
1416 return data, copy_made
1417
1418
1419def _objects_to_td64ns(
1420 data, unit=None, errors: DateTimeErrorChoices = "raise"
1421) -> np.ndarray:
1422 """
1423 Convert an object-dtyped or string-dtyped array into a
1424 timedelta64[ns]-dtyped array.
1425
1426 Parameters
1427 ----------
1428 data : ndarray or Index
1429 unit : str, default "ns"
1430 The timedelta unit to treat integers as multiples of.
1431 Must not be specified if the data contains a str.
1432 errors : {"raise", "coerce", "ignore"}, default "raise"
1433 How to handle elements that cannot be converted to timedelta64[ns].
1434 See ``pandas.to_timedelta`` for details.
1435
1436 Returns
1437 -------
1438 numpy.ndarray : timedelta64[ns] array converted from data
1439
1440 Raises
1441 ------
1442 ValueError : Data cannot be converted to timedelta64[ns].
1443
1444 Notes
1445 -----
1446 Unlike `pandas.to_timedelta`, if setting `errors=ignore` will not cause
1447 errors to be ignored; they are caught and subsequently ignored at a
1448 higher level.
1449 """
1450 # coerce Index to np.ndarray, converting string-dtype if necessary
1451 values = np.asarray(data, dtype=np.object_)
1452
1453 result = array_to_timedelta64(values, unit=unit, errors=errors)
1454 return result
1455
1456
1457def _validate_td64_dtype(dtype) -> DtypeObj:
1458 dtype = pandas_dtype(dtype)
1459 if dtype == np.dtype("m8"):
1460 # no precision disallowed GH#24806
1461 msg = (
1462 "Passing in 'timedelta' dtype with no precision is not allowed. "
1463 "Please pass in 'timedelta64[ns]' instead."
1464 )
1465 raise ValueError(msg)
1466
1467 if not lib.is_np_dtype(dtype, "m"):
1468 raise ValueError(f"dtype '{dtype}' is invalid, should be np.timedelta64 dtype")
1469 elif not is_supported_dtype(dtype):
1470 raise ValueError("Supported timedelta64 resolutions are 's', 'ms', 'us', 'ns'")
1471
1472 return dtype