1from __future__ import annotations
2
3from datetime import timedelta
4import operator
5from typing import (
6 TYPE_CHECKING,
7 Self,
8 cast,
9)
10
11import numpy as np
12
13from pandas._libs import (
14 lib,
15 tslibs,
16)
17from pandas._libs.tslibs import (
18 Day,
19 NaT,
20 NaTType,
21 Tick,
22 Timedelta,
23 astype_overflowsafe,
24 get_supported_dtype,
25 iNaT,
26 is_supported_dtype,
27 periods_per_second,
28 to_offset,
29)
30from pandas._libs.tslibs.conversion import cast_from_unit_vectorized
31from pandas._libs.tslibs.fields import (
32 get_timedelta_days,
33 get_timedelta_field,
34)
35from pandas._libs.tslibs.timedeltas import (
36 array_to_timedelta64,
37 floordiv_object_array,
38 ints_to_pytimedelta,
39 parse_timedelta_unit,
40 truediv_object_array,
41)
42from pandas.compat.numpy import function as nv
43from pandas.util._decorators import set_module
44from pandas.util._validators import validate_endpoints
45
46from pandas.core.dtypes.common import (
47 TD64NS_DTYPE,
48 is_float_dtype,
49 is_integer_dtype,
50 is_object_dtype,
51 is_scalar,
52 is_string_dtype,
53 pandas_dtype,
54)
55from pandas.core.dtypes.dtypes import (
56 ArrowDtype,
57 BaseMaskedDtype,
58 ExtensionDtype,
59)
60from pandas.core.dtypes.missing import isna
61
62from pandas.core import (
63 nanops,
64 roperator,
65)
66from pandas.core.array_algos import datetimelike_accumulations
67from pandas.core.arrays import datetimelike as dtl
68from pandas.core.arrays._ranges import generate_regular_range
69import pandas.core.common as com
70from pandas.core.ops.common import unpack_zerodim_and_defer
71
72if TYPE_CHECKING:
73 from collections.abc import Callable, Iterator
74
75 from pandas._typing import (
76 AxisInt,
77 DateTimeErrorChoices,
78 DtypeObj,
79 NpDtype,
80 npt,
81 TimeUnit,
82 )
83
84 from pandas import DataFrame
85
86import textwrap
87
88
89def _field_accessor(name: str, alias: str, docstring: str):
90 def f(self) -> np.ndarray:
91 values = self.asi8
92 if alias == "days":
93 result = get_timedelta_days(values, reso=self._creso)
94 else:
95 # error: Incompatible types in assignment (
96 # expression has type "ndarray[Any, dtype[signedinteger[_32Bit]]]",
97 # variable has type "ndarray[Any, dtype[signedinteger[_64Bit]]]
98 result = get_timedelta_field(values, alias, reso=self._creso) # type: ignore[assignment]
99 if self._hasna:
100 result = self._maybe_mask_results(
101 result, fill_value=None, convert="float64"
102 )
103
104 return result
105
106 f.__name__ = name
107 f.__doc__ = f"\n{docstring}\n"
108 return property(f)
109
110
111@set_module("pandas.arrays")
112class TimedeltaArray(dtl.TimelikeOps):
113 """
114 Pandas ExtensionArray for timedelta data.
115
116 .. warning::
117
118 TimedeltaArray is currently experimental, and its API may change
119 without warning. In particular, :attr:`TimedeltaArray.dtype` is
120 expected to change to be an instance of an ``ExtensionDtype``
121 subclass.
122
123 Parameters
124 ----------
125 data : array-like
126 The timedelta data.
127 dtype : numpy.dtype
128 Currently, only ``numpy.dtype("timedelta64[ns]")`` is accepted.
129 freq : Offset, optional
130 Frequency of the data.
131 copy : bool, default False
132 Whether to copy the underlying array of data.
133
134 Attributes
135 ----------
136 None
137
138 Methods
139 -------
140 None
141
142 See Also
143 --------
144 Timedelta : Represents a duration, the difference between two dates or times.
145 TimedeltaIndex : Immutable Index of timedelta64 data.
146 to_timedelta : Convert argument to timedelta.
147
148 Examples
149 --------
150 >>> pd.arrays.TimedeltaArray._from_sequence(pd.TimedeltaIndex(["1h", "2h"]))
151 <TimedeltaArray>
152 ['0 days 01:00:00', '0 days 02:00:00']
153 Length: 2, dtype: timedelta64[us]
154 """
155
156 _typ = "timedeltaarray"
157 _recognized_scalars = (timedelta, np.timedelta64, Tick)
158 _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: lib.is_np_dtype(x, "m")
159 _infer_matches = ("timedelta", "timedelta64")
160
161 @property
162 def _internal_fill_value(self) -> np.timedelta64:
163 return np.timedelta64("NaT", self.unit)
164
165 @property
166 def _scalar_type(self) -> type[Timedelta]:
167 return Timedelta
168
169 __array_priority__ = 1000
170 # define my properties & methods for delegation
171 _other_ops: list[str] = []
172 _bool_ops: list[str] = []
173 _field_ops: list[str] = ["days", "seconds", "microseconds", "nanoseconds"]
174 _datetimelike_ops: list[str] = _field_ops + _bool_ops + ["unit", "freq"]
175 _datetimelike_methods: list[str] = [
176 "to_pytimedelta",
177 "total_seconds",
178 "round",
179 "floor",
180 "ceil",
181 "as_unit",
182 ]
183
184 # Note: ndim must be defined to ensure NaT.__richcmp__(TimedeltaArray)
185 # operates pointwise.
186
187 def _box_func(self, x: np.timedelta64) -> Timedelta | NaTType:
188 y = x.view("i8")
189 if y == NaT._value:
190 return NaT
191 return Timedelta._from_value_and_reso(y, reso=self._creso)
192
193 @property
194 # error: Return type "dtype" of "dtype" incompatible with return type
195 # "ExtensionDtype" in supertype "ExtensionArray"
196 def dtype(self) -> np.dtype[np.timedelta64]: # type: ignore[override]
197 """
198 The dtype for the TimedeltaArray.
199
200 .. warning::
201
202 A future version of pandas will change dtype to be an instance
203 of a :class:`pandas.api.extensions.ExtensionDtype` subclass,
204 not a ``numpy.dtype``.
205
206 Returns
207 -------
208 numpy.dtype
209 """
210 return self._ndarray.dtype
211
212 # ----------------------------------------------------------------
213 # Constructors
214
215 _freq: Tick | Day | None = None
216
217 @classmethod
218 def _validate_dtype(cls, values, dtype):
219 # used in TimeLikeOps.__init__
220 dtype = _validate_td64_dtype(dtype)
221 _validate_td64_dtype(values.dtype)
222 if dtype != values.dtype:
223 raise ValueError("Values resolution does not match dtype.")
224 return dtype
225
226 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked"
227 @classmethod
228 def _simple_new( # type: ignore[override]
229 cls,
230 values: npt.NDArray[np.timedelta64],
231 freq: Tick | Day | None = None,
232 dtype: np.dtype[np.timedelta64] = TD64NS_DTYPE,
233 ) -> Self:
234 # Require td64 dtype, not unit-less, matching values.dtype
235 assert lib.is_np_dtype(dtype, "m")
236 assert not tslibs.is_unitless(dtype)
237 assert isinstance(values, np.ndarray), type(values)
238 assert dtype == values.dtype
239 assert freq is None or isinstance(freq, (Tick, Day))
240
241 result = super()._simple_new(values=values, dtype=dtype)
242 result._freq = freq
243 return result
244
245 @classmethod
246 def _from_sequence(cls, data, *, dtype=None, copy: bool = False) -> Self:
247 unit = None
248 if dtype:
249 dtype = _validate_td64_dtype(dtype)
250 if lib.infer_dtype(data) == "integer":
251 unit = np.datetime_data(dtype)[0]
252
253 data, freq = sequence_to_td64ns(data, copy=copy, unit=unit)
254
255 if dtype is not None:
256 data = astype_overflowsafe(data, dtype=dtype, copy=False)
257
258 return cls._simple_new(data, dtype=data.dtype, freq=freq)
259
260 @classmethod
261 def _from_sequence_not_strict(
262 cls,
263 data,
264 *,
265 dtype=None,
266 copy: bool = False,
267 freq=lib.no_default,
268 unit=None,
269 ) -> Self:
270 """
271 _from_sequence_not_strict but without responsibility for finding the
272 result's `freq`.
273 """
274 if dtype:
275 dtype = _validate_td64_dtype(dtype)
276 if unit is None and lib.infer_dtype(data) == "integer":
277 unit = np.datetime_data(dtype)[0]
278
279 assert unit not in ["Y", "y", "M"] # caller is responsible for checking
280
281 data, inferred_freq = sequence_to_td64ns(data, copy=copy, unit=unit)
282
283 if dtype is not None:
284 data = astype_overflowsafe(data, dtype=dtype, copy=False)
285
286 result = cls._simple_new(data, dtype=data.dtype, freq=inferred_freq)
287
288 result._maybe_pin_freq(freq, {})
289 return result
290
291 @classmethod
292 def _generate_range(
293 cls, start, end, periods, freq, closed=None, *, unit: TimeUnit
294 ) -> Self:
295 periods = dtl.validate_periods(periods)
296 if freq is None and any(x is None for x in [periods, start, end]):
297 raise ValueError("Must provide freq argument if no data is supplied")
298
299 if com.count_not_none(start, end, periods, freq) != 3:
300 raise ValueError(
301 "Of the four parameters: start, end, periods, "
302 "and freq, exactly three must be specified"
303 )
304
305 if start is not None:
306 start = Timedelta(start).as_unit("ns")
307
308 if end is not None:
309 end = Timedelta(end).as_unit("ns")
310
311 if unit not in ["s", "ms", "us", "ns"]:
312 raise ValueError("'unit' must be one of 's', 'ms', 'us', 'ns'")
313
314 if start is not None and unit is not None:
315 start = start.as_unit(unit, round_ok=False)
316 if end is not None and unit is not None:
317 end = end.as_unit(unit, round_ok=False)
318
319 left_closed, right_closed = validate_endpoints(closed)
320
321 if freq is not None:
322 index = generate_regular_range(start, end, periods, freq, unit=unit)
323 else:
324 index = np.linspace(start._value, end._value, periods).astype("i8")
325
326 if not left_closed:
327 index = index[1:]
328 if not right_closed:
329 index = index[:-1]
330
331 td64values = index.view(f"m8[{unit}]")
332 return cls._simple_new(td64values, dtype=td64values.dtype, freq=freq)
333
334 # ----------------------------------------------------------------
335 # DatetimeLike Interface
336
337 def _unbox_scalar(self, value) -> np.timedelta64:
338 if not isinstance(value, self._scalar_type) and value is not NaT:
339 raise ValueError("'value' should be a Timedelta.")
340 self._check_compatible_with(value)
341 if value is NaT:
342 return np.timedelta64(value._value, self.unit)
343 else:
344 return value.as_unit(self.unit, round_ok=False).asm8
345
346 def _scalar_from_string(self, value) -> Timedelta | NaTType:
347 return Timedelta(value)
348
349 def _check_compatible_with(self, other) -> None:
350 # we don't have anything to validate.
351 pass
352
353 # ----------------------------------------------------------------
354 # Array-Like / EA-Interface Methods
355
356 def astype(self, dtype, copy: bool = True):
357 # We handle
358 # --> timedelta64[ns]
359 # --> timedelta64
360 # DatetimeLikeArrayMixin super call handles other cases
361 dtype = pandas_dtype(dtype)
362
363 if lib.is_np_dtype(dtype, "m"):
364 if dtype == self.dtype:
365 if copy:
366 return self.copy()
367 return self
368
369 if is_supported_dtype(dtype):
370 # unit conversion e.g. timedelta64[s]
371 res_values = astype_overflowsafe(self._ndarray, dtype, copy=False)
372 return type(self)._simple_new(
373 res_values, dtype=res_values.dtype, freq=self.freq
374 )
375 else:
376 raise ValueError(
377 f"Cannot convert from {self.dtype} to {dtype}. "
378 "Supported resolutions are 's', 'ms', 'us', 'ns'"
379 )
380
381 return dtl.DatetimeLikeArrayMixin.astype(self, dtype, copy=copy)
382
383 def __iter__(self) -> Iterator:
384 if self.ndim > 1:
385 for i in range(len(self)):
386 yield self[i]
387 else:
388 # convert in chunks of 10k for efficiency
389 data = self._ndarray
390 length = len(self)
391 chunksize = 10000
392 chunks = (length // chunksize) + 1
393 for i in range(chunks):
394 start_i = i * chunksize
395 end_i = min((i + 1) * chunksize, length)
396 converted = ints_to_pytimedelta(data[start_i:end_i], box=True)
397 yield from converted
398
399 # ----------------------------------------------------------------
400 # Reductions
401
402 def sum(
403 self,
404 *,
405 axis: AxisInt | None = None,
406 dtype: NpDtype | None = None,
407 out=None,
408 keepdims: bool = False,
409 initial=None,
410 skipna: bool = True,
411 min_count: int = 0,
412 ):
413 nv.validate_sum(
414 (), {"dtype": dtype, "out": out, "keepdims": keepdims, "initial": initial}
415 )
416
417 result = nanops.nansum(
418 self._ndarray, axis=axis, skipna=skipna, min_count=min_count
419 )
420 return self._wrap_reduction_result(axis, result)
421
422 def std(
423 self,
424 *,
425 axis: AxisInt | None = None,
426 dtype: NpDtype | None = None,
427 out=None,
428 ddof: int = 1,
429 keepdims: bool = False,
430 skipna: bool = True,
431 ):
432 nv.validate_stat_ddof_func(
433 (), {"dtype": dtype, "out": out, "keepdims": keepdims}, fname="std"
434 )
435
436 result = nanops.nanstd(self._ndarray, axis=axis, skipna=skipna, ddof=ddof)
437 if axis is None or self.ndim == 1:
438 return self._box_func(result)
439 return self._from_backing_data(result)
440
441 # ----------------------------------------------------------------
442 # Accumulations
443
444 def _accumulate(self, name: str, *, skipna: bool = True, **kwargs):
445 if name == "cumsum":
446 op = getattr(datetimelike_accumulations, name)
447 result = op(self._ndarray.copy(), skipna=skipna, **kwargs)
448
449 return type(self)._simple_new(result, freq=None, dtype=self.dtype)
450 elif name == "cumprod":
451 raise TypeError("cumprod not supported for Timedelta.")
452
453 else:
454 return super()._accumulate(name, skipna=skipna, **kwargs)
455
456 # ----------------------------------------------------------------
457 # Rendering Methods
458
459 def _formatter(self, boxed: bool = False):
460 from pandas.io.formats.format import get_format_timedelta64
461
462 return get_format_timedelta64(self, box=True)
463
464 def _format_native_types(
465 self, *, na_rep: str | float = "NaT", date_format=None, **kwargs
466 ) -> npt.NDArray[np.object_]:
467 from pandas.io.formats.format import get_format_timedelta64
468
469 # Relies on TimeDelta._repr_base
470 formatter = get_format_timedelta64(self, na_rep)
471 # equiv: np.array([formatter(x) for x in self._ndarray])
472 # but independent of dimension
473 return np.frompyfunc(formatter, 1, 1)(self._ndarray)
474
475 # ----------------------------------------------------------------
476 # Arithmetic Methods
477
478 def _add_offset(self, other):
479 assert not isinstance(other, (Tick, Day))
480 raise TypeError(
481 f"cannot add the type {type(other).__name__} to a {type(self).__name__}"
482 )
483
484 @unpack_zerodim_and_defer("__mul__")
485 def __mul__(self, other) -> Self:
486 if is_scalar(other):
487 if lib.is_bool(other):
488 raise TypeError(
489 f"Cannot multiply '{self.dtype}' by bool, explicitly cast to "
490 "integers instead"
491 )
492 # numpy will accept float and int, raise TypeError for others
493 result = self._ndarray * other
494 if result.dtype.kind != "m":
495 # numpy >= 2.1 may not raise a TypeError
496 # and seems to dispatch to others.__rmul__?
497 raise TypeError(f"Cannot multiply with {type(other).__name__}")
498 freq = None
499 if self.freq is not None and not isna(other):
500 freq = self.freq * other
501 if freq.n == 0:
502 # GH#51575 Better to have no freq than an incorrect one
503 freq = None
504 return type(self)._simple_new(result, dtype=result.dtype, freq=freq)
505
506 if not hasattr(other, "dtype"):
507 # list, tuple
508 other = np.array(other)
509
510 if other.dtype.kind == "b":
511 # GH#58054
512 raise TypeError(
513 f"Cannot multiply '{self.dtype}' by bool, explicitly cast to "
514 "integers instead"
515 )
516 if isinstance(other.dtype, (ArrowDtype, BaseMaskedDtype)):
517 # GH#58054
518 return NotImplemented
519
520 if len(other) != len(self) and not lib.is_np_dtype(other.dtype, "m"):
521 # Exclude timedelta64 here so we correctly raise TypeError
522 # for that instead of ValueError
523 raise ValueError("Cannot multiply with unequal lengths")
524
525 if is_object_dtype(other.dtype):
526 # this multiplication will succeed only if all elements of other
527 # are int or float scalars, so we will end up with
528 # timedelta64[ns]-dtyped result
529 arr = self._ndarray
530 result = [arr[n] * other[n] for n in range(len(self))]
531 result = np.array(result)
532 return type(self)._simple_new(result, dtype=result.dtype)
533
534 # numpy will accept float or int dtype, raise TypeError for others
535 result = self._ndarray * other
536 if result.dtype.kind != "m":
537 # numpy >= 2.1 may not raise a TypeError
538 # and seems to dispatch to others.__rmul__?
539 raise TypeError(f"Cannot multiply with {type(other).__name__}")
540 return type(self)._simple_new(result, dtype=result.dtype)
541
542 __rmul__ = __mul__
543
544 def _scalar_divlike_op(self, other, op):
545 """
546 Shared logic for __truediv__, __rtruediv__, __floordiv__, __rfloordiv__
547 with scalar 'other'.
548 """
549 if isinstance(other, self._recognized_scalars):
550 other = Timedelta(other)
551 # mypy assumes that __new__ returns an instance of the class
552 # github.com/python/mypy/issues/1020
553 if cast("Timedelta | NaTType", other) is NaT:
554 # specifically timedelta64-NaT
555 res = np.empty(self.shape, dtype=np.float64)
556 res.fill(np.nan)
557 return res
558
559 # otherwise, dispatch to Timedelta implementation
560 return op(self._ndarray, other)
561
562 else:
563 # caller is responsible for checking lib.is_scalar(other)
564 # assume other is numeric, otherwise numpy will raise
565
566 if op in [roperator.rtruediv, roperator.rfloordiv]:
567 raise TypeError(
568 f"Cannot divide {type(other).__name__} by {type(self).__name__}"
569 )
570
571 result = op(self._ndarray, other)
572 freq = None
573
574 if self.freq is not None:
575 # Note: freq gets division, not floor-division, even if op
576 # is floordiv.
577 if isinstance(self.freq, Day):
578 if self.freq.n % other == 0:
579 freq = Day(self.freq.n // other)
580 else:
581 freq = to_offset(Timedelta(days=self.freq.n)) / other
582 else:
583 freq = self.freq / other
584 if freq.nanos == 0 and self.freq.nanos != 0:
585 # e.g. if self.freq is Nano(1) then dividing by 2
586 # rounds down to zero
587 freq = None
588
589 return type(self)._simple_new(result, dtype=result.dtype, freq=freq)
590
591 def _cast_divlike_op(self, other):
592 if not hasattr(other, "dtype"):
593 # e.g. list, tuple
594 other = np.array(other)
595
596 if len(other) != len(self):
597 raise ValueError("Cannot divide vectors with unequal lengths")
598 return other
599
600 def _vector_divlike_op(self, other, op) -> np.ndarray | Self:
601 """
602 Shared logic for __truediv__, __floordiv__, and their reversed versions
603 with timedelta64-dtype ndarray other.
604 """
605 # Let numpy handle it
606 result = op(self._ndarray, np.asarray(other))
607
608 if (is_integer_dtype(other.dtype) or is_float_dtype(other.dtype)) and op in [
609 operator.truediv,
610 operator.floordiv,
611 ]:
612 return type(self)._simple_new(result, dtype=result.dtype)
613
614 if op in [operator.floordiv, roperator.rfloordiv]:
615 mask = self.isna() | isna(other)
616 if mask.any():
617 result = result.astype(np.float64)
618 np.putmask(result, mask, np.nan)
619
620 return result
621
622 @unpack_zerodim_and_defer("__truediv__")
623 def __truediv__(self, other):
624 # timedelta / X is well-defined for timedelta-like or numeric X
625 op = operator.truediv
626 if is_scalar(other):
627 return self._scalar_divlike_op(other, op)
628
629 other = self._cast_divlike_op(other)
630 if (
631 lib.is_np_dtype(other.dtype, "m")
632 or is_integer_dtype(other.dtype)
633 or is_float_dtype(other.dtype)
634 ):
635 return self._vector_divlike_op(other, op)
636
637 if is_object_dtype(other.dtype):
638 other = np.asarray(other)
639 if self.ndim > 1:
640 res_cols = [
641 left / right for left, right in zip(self, other, strict=True)
642 ]
643 res_cols2 = [x.reshape(1, -1) for x in res_cols]
644 result = np.concatenate(res_cols2, axis=0)
645 else:
646 result = truediv_object_array(self._ndarray, other)
647
648 return result
649
650 else:
651 return NotImplemented
652
653 @unpack_zerodim_and_defer("__rtruediv__")
654 def __rtruediv__(self, other):
655 # X / timedelta is defined only for timedelta-like X
656 op = roperator.rtruediv
657 if is_scalar(other):
658 return self._scalar_divlike_op(other, op)
659
660 other = self._cast_divlike_op(other)
661 if lib.is_np_dtype(other.dtype, "m"):
662 return self._vector_divlike_op(other, op)
663
664 elif is_object_dtype(other.dtype):
665 # Note: unlike in __truediv__, we do not _need_ to do type
666 # inference on the result. It does not raise, a numeric array
667 # is returned. GH#23829
668 result_list = [other[n] / self[n] for n in range(len(self))]
669 return np.array(result_list)
670
671 else:
672 return NotImplemented
673
674 @unpack_zerodim_and_defer("__floordiv__")
675 def __floordiv__(self, other):
676 op = operator.floordiv
677 if is_scalar(other):
678 return self._scalar_divlike_op(other, op)
679
680 other = self._cast_divlike_op(other)
681 if (
682 lib.is_np_dtype(other.dtype, "m")
683 or is_integer_dtype(other.dtype)
684 or is_float_dtype(other.dtype)
685 ):
686 return self._vector_divlike_op(other, op)
687
688 elif is_object_dtype(other.dtype):
689 other = np.asarray(other)
690 if self.ndim > 1:
691 res_cols = [
692 left // right for left, right in zip(self, other, strict=True)
693 ]
694 res_cols2 = [x.reshape(1, -1) for x in res_cols]
695 result = np.concatenate(res_cols2, axis=0)
696 else:
697 result = floordiv_object_array(self._ndarray, other)
698
699 assert result.dtype == object
700 return result
701
702 else:
703 return NotImplemented
704
705 @unpack_zerodim_and_defer("__rfloordiv__")
706 def __rfloordiv__(self, other):
707 op = roperator.rfloordiv
708 if is_scalar(other):
709 return self._scalar_divlike_op(other, op)
710
711 other = self._cast_divlike_op(other)
712 if lib.is_np_dtype(other.dtype, "m"):
713 return self._vector_divlike_op(other, op)
714
715 elif is_object_dtype(other.dtype):
716 result_list = [other[n] // self[n] for n in range(len(self))]
717 result = np.array(result_list)
718 return result
719
720 else:
721 return NotImplemented
722
723 @unpack_zerodim_and_defer("__mod__")
724 def __mod__(self, other):
725 # Note: This is a naive implementation, can likely be optimized
726 if isinstance(other, self._recognized_scalars):
727 other = Timedelta(other)
728 return self - (self // other) * other
729
730 @unpack_zerodim_and_defer("__rmod__")
731 def __rmod__(self, other):
732 # Note: This is a naive implementation, can likely be optimized
733 if isinstance(other, self._recognized_scalars):
734 other = Timedelta(other)
735 return other - (other // self) * self
736
737 @unpack_zerodim_and_defer("__divmod__")
738 def __divmod__(self, other):
739 # Note: This is a naive implementation, can likely be optimized
740 if isinstance(other, self._recognized_scalars):
741 other = Timedelta(other)
742
743 res1 = self // other
744 res2 = self - res1 * other
745 return res1, res2
746
747 @unpack_zerodim_and_defer("__rdivmod__")
748 def __rdivmod__(self, other):
749 # Note: This is a naive implementation, can likely be optimized
750 if isinstance(other, self._recognized_scalars):
751 other = Timedelta(other)
752
753 res1 = other // self
754 res2 = other - res1 * self
755 return res1, res2
756
757 def __neg__(self) -> TimedeltaArray:
758 freq = None
759 if self.freq is not None:
760 freq = -self.freq
761 return type(self)._simple_new(-self._ndarray, dtype=self.dtype, freq=freq)
762
763 def __pos__(self) -> TimedeltaArray:
764 return type(self)._simple_new(
765 self._ndarray.copy(), dtype=self.dtype, freq=self.freq
766 )
767
768 def __abs__(self) -> TimedeltaArray:
769 # Note: freq is not preserved
770 return type(self)._simple_new(np.abs(self._ndarray), dtype=self.dtype)
771
772 # ----------------------------------------------------------------
773 # Conversion Methods - Vectorized analogues of Timedelta methods
774
775 def total_seconds(self) -> npt.NDArray[np.float64]:
776 """
777 Return total duration of each element expressed in seconds.
778
779 This method is available directly on TimedeltaArray, TimedeltaIndex
780 and on Series containing timedelta values under the ``.dt`` namespace.
781
782 Returns
783 -------
784 ndarray, Index or Series
785 When the calling object is a TimedeltaArray, the return type
786 is ndarray. When the calling object is a TimedeltaIndex,
787 the return type is an Index with a float64 dtype. When the calling object
788 is a Series, the return type is Series of type `float64` whose
789 index is the same as the original.
790
791 See Also
792 --------
793 datetime.timedelta.total_seconds : Standard library version
794 of this method.
795 TimedeltaIndex.components : Return a DataFrame with components of
796 each Timedelta.
797
798 Examples
799 --------
800 **Series**
801
802 >>> s = pd.Series(pd.to_timedelta(np.arange(5), unit="D"))
803 >>> s
804 0 0 days
805 1 1 days
806 2 2 days
807 3 3 days
808 4 4 days
809 dtype: timedelta64[s]
810
811 >>> s.dt.total_seconds()
812 0 0.0
813 1 86400.0
814 2 172800.0
815 3 259200.0
816 4 345600.0
817 dtype: float64
818
819 **TimedeltaIndex**
820
821 >>> idx = pd.to_timedelta(np.arange(5), unit="D")
822 >>> idx
823 TimedeltaIndex(['0 days', '1 days', '2 days', '3 days', '4 days'],
824 dtype='timedelta64[s]', freq=None)
825
826 >>> idx.total_seconds()
827 Index([0.0, 86400.0, 172800.0, 259200.0, 345600.0], dtype='float64')
828 """
829 pps = periods_per_second(self._creso)
830 return self._maybe_mask_results(self.asi8 / pps, fill_value=None)
831
832 def to_pytimedelta(self) -> npt.NDArray[np.object_]:
833 """
834 Return an ndarray of datetime.timedelta objects.
835
836 Returns
837 -------
838 numpy.ndarray
839 A NumPy ``timedelta64`` object representing the same duration as the
840 original pandas ``Timedelta`` object. The precision of the resulting
841 object is in nanoseconds, which is the default
842 time resolution used by pandas for ``Timedelta`` objects, ensuring
843 high precision for time-based calculations.
844
845 See Also
846 --------
847 to_timedelta : Convert argument to timedelta format.
848 Timedelta : Represents a duration between two dates or times.
849 DatetimeIndex: Index of datetime64 data.
850 Timedelta.components : Return a components namedtuple-like
851 of a single timedelta.
852
853 Examples
854 --------
855 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
856 >>> tdelta_idx
857 TimedeltaIndex(['1 days', '2 days', '3 days'],
858 dtype='timedelta64[s]', freq=None)
859 >>> tdelta_idx.to_pytimedelta()
860 array([datetime.timedelta(days=1), datetime.timedelta(days=2),
861 datetime.timedelta(days=3)], dtype=object)
862
863 >>> tidx = pd.TimedeltaIndex(data=["1 days 02:30:45", "3 days 04:15:10"])
864 >>> tidx
865 TimedeltaIndex(['1 days 02:30:45', '3 days 04:15:10'],
866 dtype='timedelta64[us]', freq=None)
867 >>> tidx.to_pytimedelta()
868 array([datetime.timedelta(days=1, seconds=9045),
869 datetime.timedelta(days=3, seconds=15310)], dtype=object)
870 """
871 return ints_to_pytimedelta(self._ndarray)
872
873 days_docstring = textwrap.dedent(
874 """Number of days for each element.
875
876 See Also
877 --------
878 Series.dt.seconds : Return number of seconds for each element.
879 Series.dt.microseconds : Return number of microseconds for each element.
880 Series.dt.nanoseconds : Return number of nanoseconds for each element.
881
882 Examples
883 --------
884 For Series:
885
886 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='D'))
887 >>> ser
888 0 1 days
889 1 2 days
890 2 3 days
891 dtype: timedelta64[s]
892 >>> ser.dt.days
893 0 1
894 1 2
895 2 3
896 dtype: int64
897
898 For TimedeltaIndex:
899
900 >>> tdelta_idx = pd.to_timedelta(["0 days", "10 days", "20 days"])
901 >>> tdelta_idx
902 TimedeltaIndex(['0 days', '10 days', '20 days'],
903 dtype='timedelta64[us]', freq=None)
904 >>> tdelta_idx.days
905 Index([0, 10, 20], dtype='int64')"""
906 )
907 days = _field_accessor("days", "days", days_docstring)
908
909 seconds_docstring = textwrap.dedent(
910 """Number of seconds (>= 0 and less than 1 day) for each element.
911
912 See Also
913 --------
914 Series.dt.seconds : Return number of seconds for each element.
915 Series.dt.nanoseconds : Return number of nanoseconds for each element.
916
917 Examples
918 --------
919 For Series:
920
921 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='s'))
922 >>> ser
923 0 0 days 00:00:01
924 1 0 days 00:00:02
925 2 0 days 00:00:03
926 dtype: timedelta64[s]
927 >>> ser.dt.seconds
928 0 1
929 1 2
930 2 3
931 dtype: int32
932
933 For TimedeltaIndex:
934
935 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='s')
936 >>> tdelta_idx
937 TimedeltaIndex(['0 days 00:00:01', '0 days 00:00:02', '0 days 00:00:03'],
938 dtype='timedelta64[s]', freq=None)
939 >>> tdelta_idx.seconds
940 Index([1, 2, 3], dtype='int32')"""
941 )
942 seconds = _field_accessor(
943 "seconds",
944 "seconds",
945 seconds_docstring,
946 )
947
948 microseconds_docstring = textwrap.dedent(
949 """Number of microseconds (>= 0 and less than 1 second) for each element.
950
951 See Also
952 --------
953 pd.Timedelta.microseconds : Number of microseconds (>= 0 and less than 1 second).
954 pd.Timedelta.to_pytimedelta.microseconds : Number of microseconds (>= 0 and less
955 than 1 second) of a datetime.timedelta.
956
957 Examples
958 --------
959 For Series:
960
961 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='us'))
962 >>> ser
963 0 0 days 00:00:00.000001
964 1 0 days 00:00:00.000002
965 2 0 days 00:00:00.000003
966 dtype: timedelta64[us]
967 >>> ser.dt.microseconds
968 0 1
969 1 2
970 2 3
971 dtype: int32
972
973 For TimedeltaIndex:
974
975 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='us')
976 >>> tdelta_idx
977 TimedeltaIndex(['0 days 00:00:00.000001', '0 days 00:00:00.000002',
978 '0 days 00:00:00.000003'],
979 dtype='timedelta64[us]', freq=None)
980 >>> tdelta_idx.microseconds
981 Index([1, 2, 3], dtype='int32')"""
982 )
983 microseconds = _field_accessor(
984 "microseconds",
985 "microseconds",
986 microseconds_docstring,
987 )
988
989 nanoseconds_docstring = textwrap.dedent(
990 """Number of nanoseconds (>= 0 and less than 1 microsecond) for each element.
991
992 See Also
993 --------
994 Series.dt.seconds : Return number of seconds for each element.
995 Series.dt.microseconds : Return number of nanoseconds for each element.
996
997 Examples
998 --------
999 For Series:
1000
1001 >>> ser = pd.Series(pd.to_timedelta([1, 2, 3], unit='ns'))
1002 >>> ser
1003 0 0 days 00:00:00.000000001
1004 1 0 days 00:00:00.000000002
1005 2 0 days 00:00:00.000000003
1006 dtype: timedelta64[ns]
1007 >>> ser.dt.nanoseconds
1008 0 1
1009 1 2
1010 2 3
1011 dtype: int32
1012
1013 For TimedeltaIndex:
1014
1015 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit='ns')
1016 >>> tdelta_idx
1017 TimedeltaIndex(['0 days 00:00:00.000000001', '0 days 00:00:00.000000002',
1018 '0 days 00:00:00.000000003'],
1019 dtype='timedelta64[ns]', freq=None)
1020 >>> tdelta_idx.nanoseconds
1021 Index([1, 2, 3], dtype='int32')"""
1022 )
1023 nanoseconds = _field_accessor(
1024 "nanoseconds",
1025 "nanoseconds",
1026 nanoseconds_docstring,
1027 )
1028
1029 @property
1030 def components(self) -> DataFrame:
1031 """
1032 Return a DataFrame of the individual resolution components of the Timedeltas.
1033
1034 The components (days, hours, minutes seconds, milliseconds, microseconds,
1035 nanoseconds) are returned as columns in a DataFrame.
1036
1037 Returns
1038 -------
1039 DataFrame
1040
1041 See Also
1042 --------
1043 TimedeltaIndex.total_seconds : Return total duration expressed in seconds.
1044 Timedelta.components : Return a components namedtuple-like of a single
1045 timedelta.
1046
1047 Examples
1048 --------
1049 >>> tdelta_idx = pd.to_timedelta(["1 day 3 min 2 us 42 ns"])
1050 >>> tdelta_idx
1051 TimedeltaIndex(['1 days 00:03:00.000002042'],
1052 dtype='timedelta64[ns]', freq=None)
1053 >>> tdelta_idx.components
1054 days hours minutes seconds milliseconds microseconds nanoseconds
1055 0 1 0 3 0 0 2 42
1056 """
1057 from pandas import DataFrame
1058
1059 columns = [
1060 "days",
1061 "hours",
1062 "minutes",
1063 "seconds",
1064 "milliseconds",
1065 "microseconds",
1066 "nanoseconds",
1067 ]
1068 hasnans = self._hasna
1069 if hasnans:
1070
1071 def f(x):
1072 if isna(x):
1073 return [np.nan] * len(columns)
1074 return x.components
1075
1076 else:
1077
1078 def f(x):
1079 return x.components
1080
1081 result = DataFrame([f(x) for x in self], columns=columns)
1082 if not hasnans:
1083 result = result.astype("int64")
1084 return result
1085
1086
1087# ---------------------------------------------------------------------
1088# Constructor Helpers
1089
1090
1091def sequence_to_td64ns(
1092 data,
1093 copy: bool = False,
1094 unit=None,
1095 errors: DateTimeErrorChoices = "raise",
1096) -> tuple[np.ndarray, Tick | Day | None]:
1097 """
1098 Parameters
1099 ----------
1100 data : list-like
1101 copy : bool, default False
1102 unit : str, optional
1103 The timedelta unit to treat integers as multiples of. For numeric
1104 data this defaults to ``'ns'``.
1105 Must be un-specified if the data contains a str and ``errors=="raise"``.
1106 errors : {"raise", "coerce", "ignore"}, default "raise"
1107 How to handle elements that cannot be converted to timedelta64[ns].
1108 See ``pandas.to_timedelta`` for details.
1109
1110 Returns
1111 -------
1112 converted : numpy.ndarray
1113 The sequence converted to a numpy array with dtype ``timedelta64[ns]``.
1114 inferred_freq : Tick, Day, or None
1115 The inferred frequency of the sequence.
1116
1117 Raises
1118 ------
1119 ValueError : Data cannot be converted to timedelta64[ns].
1120
1121 Notes
1122 -----
1123 Unlike `pandas.to_timedelta`, if setting ``errors=ignore`` will not cause
1124 errors to be ignored; they are caught and subsequently ignored at a
1125 higher level.
1126 """
1127 assert unit not in ["Y", "y", "M"] # caller is responsible for checking
1128
1129 inferred_freq = None
1130 if unit is not None:
1131 unit = parse_timedelta_unit(unit)
1132
1133 data, copy = dtl.ensure_arraylike_for_datetimelike(
1134 data, copy, cls_name="TimedeltaArray"
1135 )
1136
1137 if isinstance(data, TimedeltaArray):
1138 inferred_freq = data.freq
1139
1140 # Convert whatever we have into timedelta64[ns] dtype
1141 if data.dtype == object or is_string_dtype(data.dtype):
1142 # no need to make a copy, need to convert if string-dtyped
1143 data = _objects_to_td64ns(data, unit=unit, errors=errors)
1144 copy = False
1145
1146 elif is_integer_dtype(data.dtype):
1147 # treat as multiples of the given unit
1148 data, copy_made = _ints_to_td64ns(data, unit=unit)
1149 copy = copy and not copy_made
1150
1151 elif is_float_dtype(data.dtype):
1152 # cast the unit, multiply base/frac separately
1153 # to avoid precision issues from float -> int
1154 if isinstance(data.dtype, ExtensionDtype):
1155 mask = data._mask
1156 data = data._data
1157 else:
1158 mask = np.isnan(data)
1159
1160 if unit is not None and unit != "ns":
1161 # if all non-NaN entries are round, treat these like ints and give
1162 # back the requested unit (or closest-supported)
1163 with np.errstate(invalid="ignore"):
1164 int_data = data.astype(np.int64)
1165 all_round = (mask | (data == int_data)).all()
1166 if all_round:
1167 result, _ = sequence_to_td64ns(
1168 int_data, copy=False, unit=unit, errors=errors
1169 )
1170 result[mask] = iNaT
1171 return result, inferred_freq
1172
1173 data = cast_from_unit_vectorized(data, unit or "ns")
1174 data[mask] = iNaT
1175 data = data.view("m8[ns]")
1176 copy = False
1177
1178 elif lib.is_np_dtype(data.dtype, "m"):
1179 if not is_supported_dtype(data.dtype):
1180 # cast to closest supported unit, i.e. s or ns
1181 new_dtype = get_supported_dtype(data.dtype)
1182 data = astype_overflowsafe(data, dtype=new_dtype, copy=False)
1183 copy = False
1184
1185 else:
1186 # This includes datetime64-dtype, see GH#23539, GH#29794
1187 raise TypeError(f"dtype {data.dtype} cannot be converted to timedelta64[ns]")
1188
1189 if not copy:
1190 data = np.asarray(data)
1191 else:
1192 data = np.array(data, copy=copy)
1193
1194 assert data.dtype.kind == "m"
1195 assert data.dtype != "m8" # i.e. not unit-less
1196
1197 return data, inferred_freq
1198
1199
1200def _ints_to_td64ns(data, unit: str = "ns") -> tuple[np.ndarray, bool]:
1201 """
1202 Convert an ndarray with integer-dtype to timedelta64[ns] dtype, treating
1203 the integers as multiples of the given timedelta unit.
1204
1205 Parameters
1206 ----------
1207 data : numpy.ndarray with integer-dtype
1208 unit : str, default "ns"
1209 The timedelta unit to treat integers as multiples of.
1210
1211 Returns
1212 -------
1213 numpy.ndarray : timedelta64[ns] array converted from data
1214 bool : whether a copy was made
1215 """
1216 copy_made = False
1217 unit = unit if unit is not None else "ns"
1218
1219 if data.dtype != np.int64:
1220 # converting to int64 makes a copy, so we can avoid
1221 # re-copying later
1222 data = data.astype(np.int64)
1223 copy_made = True
1224
1225 if unit != "ns":
1226 dtype_str = f"timedelta64[{unit}]"
1227 data = data.view(dtype_str)
1228
1229 new_dtype = get_supported_dtype(data.dtype)
1230 if new_dtype != data.dtype:
1231 data = astype_overflowsafe(data, dtype=new_dtype)
1232
1233 # the astype conversion makes a copy, so we can avoid re-copying later
1234 copy_made = True
1235
1236 else:
1237 data = data.view("timedelta64[ns]")
1238
1239 return data, copy_made
1240
1241
1242def _objects_to_td64ns(
1243 data, unit=None, errors: DateTimeErrorChoices = "raise"
1244) -> np.ndarray:
1245 """
1246 Convert an object-dtyped or string-dtyped array into a
1247 timedelta64[ns]-dtyped array.
1248
1249 Parameters
1250 ----------
1251 data : ndarray or Index
1252 unit : str, default "ns"
1253 The timedelta unit to treat integers as multiples of.
1254 Must not be specified if the data contains a str.
1255 errors : {"raise", "coerce", "ignore"}, default "raise"
1256 How to handle elements that cannot be converted to timedelta64[ns].
1257 See ``pandas.to_timedelta`` for details.
1258
1259 Returns
1260 -------
1261 numpy.ndarray : timedelta64[ns] array converted from data
1262
1263 Raises
1264 ------
1265 ValueError : Data cannot be converted to timedelta64[ns].
1266
1267 Notes
1268 -----
1269 Unlike `pandas.to_timedelta`, if setting `errors=ignore` will not cause
1270 errors to be ignored; they are caught and subsequently ignored at a
1271 higher level.
1272 """
1273 # coerce Index to np.ndarray, converting string-dtype if necessary
1274 values = np.asarray(data, dtype=np.object_)
1275
1276 result = array_to_timedelta64(values, unit=unit, errors=errors)
1277 return result
1278
1279
1280def _validate_td64_dtype(dtype) -> DtypeObj:
1281 dtype = pandas_dtype(dtype)
1282 if dtype == np.dtype("m8"):
1283 # no precision disallowed GH#24806
1284 msg = (
1285 "Passing in 'timedelta' dtype with no precision is not allowed. "
1286 "Please pass in 'timedelta64[ns]' instead."
1287 )
1288 raise ValueError(msg)
1289
1290 if not lib.is_np_dtype(dtype, "m"):
1291 raise ValueError(f"dtype '{dtype}' is invalid, should be np.timedelta64 dtype")
1292 elif not is_supported_dtype(dtype):
1293 raise ValueError("Supported timedelta64 resolutions are 's', 'ms', 'us', 'ns'")
1294
1295 return dtype