import collections import warnings from pandas.util._exceptions import find_stack_level cimport cython from cpython.object cimport ( Py_EQ, Py_NE, PyObject, PyObject_RichCompare, ) import numpy as np cimport numpy as cnp from numpy cimport ( int64_t, ndarray, ) cnp.import_array() from cpython.datetime cimport ( PyDateTime_Check, PyDelta_Check, import_datetime, timedelta, ) import_datetime() cimport pandas._libs.tslibs.util as util from pandas._libs.tslibs.base cimport ABCTimestamp from pandas._libs.tslibs.conversion cimport ( cast_from_unit, precision_from_unit, ) from pandas._libs.tslibs.dtypes cimport npy_unit_to_abbrev from pandas._libs.tslibs.nattype cimport ( NPY_NAT, c_NaT as NaT, c_nat_strings as nat_strings, checknull_with_nat, ) from pandas._libs.tslibs.np_datetime cimport ( NPY_DATETIMEUNIT, NPY_FR_ns, cmp_dtstructs, cmp_scalar, convert_reso, get_conversion_factor, get_datetime64_unit, get_timedelta64_value, get_unit_from_dtype, npy_datetimestruct, pandas_datetime_to_datetimestruct, pandas_timedelta_to_timedeltastruct, pandas_timedeltastruct, ) from pandas._libs.tslibs.np_datetime import ( OutOfBoundsDatetime, OutOfBoundsTimedelta, ) from pandas._libs.tslibs.offsets cimport is_tick_object from pandas._libs.tslibs.util cimport ( is_array, is_datetime64_object, is_float_object, is_integer_object, is_timedelta64_object, ) from pandas._libs.tslibs.fields import ( RoundTo, round_nsint64, ) # ---------------------------------------------------------------------- # Constants # components named tuple Components = collections.namedtuple( "Components", [ "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "nanoseconds", ], ) # This should be kept consistent with UnitChoices in pandas/_libs/tslibs/timedeltas.pyi cdef dict timedelta_abbrevs = { "Y": "Y", "y": "Y", "M": "M", "W": "W", "w": "W", "D": "D", "d": "D", "days": "D", "day": "D", "hours": "h", "hour": "h", "hr": "h", "h": "h", "m": "m", "minute": "m", "min": "m", "minutes": "m", "t": "m", "s": "s", "seconds": "s", "sec": "s", "second": "s", "ms": "ms", "milliseconds": "ms", "millisecond": "ms", "milli": "ms", "millis": "ms", "l": "ms", "us": "us", "microseconds": "us", "microsecond": "us", "µs": "us", "micro": "us", "micros": "us", "u": "us", "ns": "ns", "nanoseconds": "ns", "nano": "ns", "nanos": "ns", "nanosecond": "ns", "n": "ns", } _no_input = object() # ---------------------------------------------------------------------- # API @cython.boundscheck(False) @cython.wraparound(False) def ints_to_pytimedelta(ndarray m8values, box=False): """ convert an i8 repr to an ndarray of timedelta or Timedelta (if box == True) Parameters ---------- arr : ndarray[timedelta64] box : bool, default False Returns ------- result : ndarray[object] array of Timedelta or timedeltas objects """ cdef: NPY_DATETIMEUNIT reso = get_unit_from_dtype(m8values.dtype) Py_ssize_t i, n = m8values.size int64_t value object res_val # Note that `result` (and thus `result_flat`) is C-order and # `it` iterates C-order as well, so the iteration matches # See discussion at # github.com/pandas-dev/pandas/pull/46886#discussion_r860261305 ndarray result = cnp.PyArray_EMPTY(m8values.ndim, m8values.shape, cnp.NPY_OBJECT, 0) object[::1] res_flat = result.ravel() # should NOT be a copy ndarray arr = m8values.view("i8") cnp.flatiter it = cnp.PyArray_IterNew(arr) for i in range(n): # Analogous to: value = arr[i] value = (cnp.PyArray_ITER_DATA(it))[0] if value == NPY_NAT: res_val = NaT else: if box: res_val = _timedelta_from_value_and_reso(Timedelta, value, reso=reso) elif reso == NPY_DATETIMEUNIT.NPY_FR_ns: res_val = timedelta(microseconds=int(value) / 1000) elif reso == NPY_DATETIMEUNIT.NPY_FR_us: res_val = timedelta(microseconds=value) elif reso == NPY_DATETIMEUNIT.NPY_FR_ms: res_val = timedelta(milliseconds=value) elif reso == NPY_DATETIMEUNIT.NPY_FR_s: res_val = timedelta(seconds=value) elif reso == NPY_DATETIMEUNIT.NPY_FR_m: res_val = timedelta(minutes=value) elif reso == NPY_DATETIMEUNIT.NPY_FR_h: res_val = timedelta(hours=value) elif reso == NPY_DATETIMEUNIT.NPY_FR_D: res_val = timedelta(days=value) elif reso == NPY_DATETIMEUNIT.NPY_FR_W: res_val = timedelta(weeks=value) else: # Month, Year, NPY_FR_GENERIC, pico, femto, atto raise NotImplementedError(reso) # Note: we can index result directly instead of using PyArray_MultiIter_DATA # like we do for the other functions because result is known C-contiguous # and is the first argument to PyArray_MultiIterNew2. The usual pattern # does not seem to work with object dtype. # See discussion at # github.com/pandas-dev/pandas/pull/46886#discussion_r860261305 res_flat[i] = res_val cnp.PyArray_ITER_NEXT(it) return result # ---------------------------------------------------------------------- cpdef int64_t delta_to_nanoseconds( delta, NPY_DATETIMEUNIT reso=NPY_FR_ns, bint round_ok=True, ) except? -1: # Note: this will raise on timedelta64 with Y or M unit cdef: NPY_DATETIMEUNIT in_reso int64_t n if is_tick_object(delta): n = delta.n in_reso = delta._reso elif isinstance(delta, _Timedelta): n = delta.value in_reso = delta._reso elif is_timedelta64_object(delta): in_reso = get_datetime64_unit(delta) if in_reso == NPY_DATETIMEUNIT.NPY_FR_Y or in_reso == NPY_DATETIMEUNIT.NPY_FR_M: raise ValueError( "delta_to_nanoseconds does not support Y or M units, " "as their duration in nanoseconds is ambiguous." ) n = get_timedelta64_value(delta) elif PyDelta_Check(delta): in_reso = NPY_DATETIMEUNIT.NPY_FR_us try: n = ( delta.days * 24 * 3600 * 1_000_000 + delta.seconds * 1_000_000 + delta.microseconds ) except OverflowError as err: raise OutOfBoundsTimedelta(*err.args) from err else: raise TypeError(type(delta)) try: return convert_reso(n, in_reso, reso, round_ok=round_ok) except (OutOfBoundsDatetime, OverflowError) as err: # Catch OutOfBoundsDatetime bc convert_reso can call check_dts_bounds # for Y/M-resolution cases unit_str = npy_unit_to_abbrev(reso) raise OutOfBoundsTimedelta( f"Cannot cast {str(delta)} to unit={unit_str} without overflow." ) from err @cython.overflowcheck(True) cdef object ensure_td64ns(object ts): """ Overflow-safe implementation of td64.astype("m8[ns]") Parameters ---------- ts : np.timedelta64 Returns ------- np.timedelta64[ns] """ cdef: NPY_DATETIMEUNIT td64_unit int64_t td64_value, mult str unitstr td64_unit = get_datetime64_unit(ts) if ( td64_unit != NPY_DATETIMEUNIT.NPY_FR_ns and td64_unit != NPY_DATETIMEUNIT.NPY_FR_GENERIC ): unitstr = npy_unit_to_abbrev(td64_unit) td64_value = get_timedelta64_value(ts) mult = precision_from_unit(unitstr)[0] try: # NB: cython#1381 this cannot be *= td64_value = td64_value * mult except OverflowError as err: raise OutOfBoundsTimedelta(ts) from err return np.timedelta64(td64_value, "ns") return ts cdef convert_to_timedelta64(object ts, str unit): """ Convert an incoming object to a timedelta64 if possible. Before calling, unit must be standardized to avoid repeated unit conversion Handle these types of objects: - timedelta/Timedelta - timedelta64 - an offset - np.int64 (with unit providing a possible modifier) - None/NaT Return an ns based int64 """ # Caller is responsible for checking unit not in ["Y", "y", "M"] if checknull_with_nat(ts): return np.timedelta64(NPY_NAT, "ns") elif isinstance(ts, _Timedelta): # already in the proper format if ts._reso != NPY_FR_ns: raise NotImplementedError ts = np.timedelta64(ts.value, "ns") elif is_timedelta64_object(ts): ts = ensure_td64ns(ts) elif is_integer_object(ts): if ts == NPY_NAT: return np.timedelta64(NPY_NAT, "ns") else: ts = _maybe_cast_from_unit(ts, unit) elif is_float_object(ts): ts = _maybe_cast_from_unit(ts, unit) elif isinstance(ts, str): if (len(ts) > 0 and ts[0] == "P") or (len(ts) > 1 and ts[:2] == "-P"): ts = parse_iso_format_string(ts) else: ts = parse_timedelta_string(ts) ts = np.timedelta64(ts, "ns") elif is_tick_object(ts): ts = np.timedelta64(ts.nanos, "ns") if PyDelta_Check(ts): ts = np.timedelta64(delta_to_nanoseconds(ts), "ns") elif not is_timedelta64_object(ts): raise ValueError(f"Invalid type for timedelta scalar: {type(ts)}") return ts.astype("timedelta64[ns]") cdef _maybe_cast_from_unit(ts, str unit): # caller is responsible for checking # assert unit not in ["Y", "y", "M"] try: ts = cast_from_unit(ts, unit) except OverflowError as err: raise OutOfBoundsTimedelta( f"Cannot cast {ts} from {unit} to 'ns' without overflow." ) from err ts = np.timedelta64(ts, "ns") return ts @cython.boundscheck(False) @cython.wraparound(False) def array_to_timedelta64( ndarray values, str unit=None, str errors="raise" ) -> ndarray: # values is object-dtype, may be 2D """ Convert an ndarray to an array of timedeltas. If errors == 'coerce', coerce non-convertible objects to NaT. Otherwise, raise. Returns ------- np.ndarray[timedelta64ns] """ # Caller is responsible for checking assert unit not in ["Y", "y", "M"] cdef: Py_ssize_t i, n = values.size ndarray result = np.empty((values).shape, dtype="m8[ns]") object item int64_t ival cnp.broadcast mi = cnp.PyArray_MultiIterNew2(result, values) cnp.flatiter it if values.descr.type_num != cnp.NPY_OBJECT: # raise here otherwise we segfault below raise TypeError("array_to_timedelta64 'values' must have object dtype") if errors not in {'ignore', 'raise', 'coerce'}: raise ValueError("errors must be one of {'ignore', 'raise', or 'coerce'}") if unit is not None and errors != "coerce": it = cnp.PyArray_IterNew(values) for i in range(n): # Analogous to: item = values[i] item = cnp.PyArray_GETITEM(values, cnp.PyArray_ITER_DATA(it)) if isinstance(item, str): raise ValueError( "unit must not be specified if the input contains a str" ) cnp.PyArray_ITER_NEXT(it) # Usually, we have all strings. If so, we hit the fast path. # If this path fails, we try conversion a different way, and # this is where all of the error handling will take place. try: for i in range(n): # Analogous to: item = values[i] item = (cnp.PyArray_MultiIter_DATA(mi, 1))[0] ival = _item_to_timedelta64_fastpath(item) # Analogous to: iresult[i] = ival (cnp.PyArray_MultiIter_DATA(mi, 0))[0] = ival cnp.PyArray_MultiIter_NEXT(mi) except (TypeError, ValueError): cnp.PyArray_MultiIter_RESET(mi) parsed_unit = parse_timedelta_unit(unit or 'ns') for i in range(n): item = (cnp.PyArray_MultiIter_DATA(mi, 1))[0] ival = _item_to_timedelta64(item, parsed_unit, errors) (cnp.PyArray_MultiIter_DATA(mi, 0))[0] = ival cnp.PyArray_MultiIter_NEXT(mi) return result cdef inline int64_t _item_to_timedelta64_fastpath(object item) except? -1: """ See array_to_timedelta64. """ if item is NaT: # we allow this check in the fast-path because NaT is a C-object # so this is an inexpensive check return NPY_NAT else: return parse_timedelta_string(item) cdef inline int64_t _item_to_timedelta64(object item, str parsed_unit, str errors) except? -1: """ See array_to_timedelta64. """ try: return get_timedelta64_value(convert_to_timedelta64(item, parsed_unit)) except ValueError as err: if errors == "coerce": return NPY_NAT elif "unit abbreviation w/o a number" in str(err): # re-raise with more pertinent message msg = f"Could not convert '{item}' to NumPy timedelta" raise ValueError(msg) from err else: raise cdef inline int64_t parse_timedelta_string(str ts) except? -1: """ Parse a regular format timedelta string. Return an int64_t (in ns) or raise a ValueError on an invalid parse. """ cdef: unicode c bint neg = 0, have_dot = 0, have_value = 0, have_hhmmss = 0 object current_unit = None int64_t result = 0, m = 0, r list number = [], frac = [], unit = [] # neg : tracks if we have a leading negative for the value # have_dot : tracks if we are processing a dot (either post hhmmss or # inside an expression) # have_value : track if we have at least 1 leading unit # have_hhmmss : tracks if we have a regular format hh:mm:ss if len(ts) == 0 or ts in nat_strings: return NPY_NAT for c in ts: # skip whitespace / commas if c == ' ' or c == ',': pass # positive signs are ignored elif c == '+': pass # neg elif c == '-': if neg or have_value or have_hhmmss: raise ValueError("only leading negative signs are allowed") neg = 1 # number (ascii codes) elif ord(c) >= 48 and ord(c) <= 57: if have_dot: # we found a dot, but now its just a fraction if len(unit): number.append(c) have_dot = 0 else: frac.append(c) elif not len(unit): number.append(c) else: r = timedelta_from_spec(number, frac, unit) unit, number, frac = [], [c], [] result += timedelta_as_neg(r, neg) # hh:mm:ss. elif c == ':': # we flip this off if we have a leading value if have_value: neg = 0 # we are in the pattern hh:mm:ss pattern if len(number): if current_unit is None: current_unit = 'h' m = 1000000000 * 3600 elif current_unit == 'h': current_unit = 'm' m = 1000000000 * 60 elif current_unit == 'm': current_unit = 's' m = 1000000000 r = int(''.join(number)) * m result += timedelta_as_neg(r, neg) have_hhmmss = 1 else: raise ValueError(f"expecting hh:mm:ss format, received: {ts}") unit, number = [], [] # after the decimal point elif c == '.': if len(number) and current_unit is not None: # by definition we had something like # so we need to evaluate the final field from a # hh:mm:ss (so current_unit is 'm') if current_unit != 'm': raise ValueError("expected hh:mm:ss format before .") m = 1000000000 r = int(''.join(number)) * m result += timedelta_as_neg(r, neg) have_value = 1 unit, number, frac = [], [], [] have_dot = 1 # unit else: unit.append(c) have_value = 1 have_dot = 0 # we had a dot, but we have a fractional # value since we have an unit if have_dot and len(unit): r = timedelta_from_spec(number, frac, unit) result += timedelta_as_neg(r, neg) # we have a dot as part of a regular format # e.g. hh:mm:ss.fffffff elif have_dot: if ((len(number) or len(frac)) and not len(unit) and current_unit is None): raise ValueError("no units specified") if len(frac) > 0 and len(frac) <= 3: m = 10**(3 -len(frac)) * 1000 * 1000 elif len(frac) > 3 and len(frac) <= 6: m = 10**(6 -len(frac)) * 1000 elif len(frac) > 6 and len(frac) <= 9: m = 10**(9 -len(frac)) else: m = 1 frac = frac[:9] r = int(''.join(frac)) * m result += timedelta_as_neg(r, neg) # we have a regular format # we must have seconds at this point (hence the unit is still 'm') elif current_unit is not None: if current_unit != 'm': raise ValueError("expected hh:mm:ss format") m = 1000000000 r = int(''.join(number)) * m result += timedelta_as_neg(r, neg) # we have a last abbreviation elif len(unit): if len(number): r = timedelta_from_spec(number, frac, unit) result += timedelta_as_neg(r, neg) else: raise ValueError("unit abbreviation w/o a number") # we only have symbols and no numbers elif len(number) == 0: raise ValueError("symbols w/o a number") # treat as nanoseconds # but only if we don't have anything else else: if have_value: raise ValueError("have leftover units") if len(number): r = timedelta_from_spec(number, frac, 'ns') result += timedelta_as_neg(r, neg) return result cdef inline int64_t timedelta_as_neg(int64_t value, bint neg): """ Parameters ---------- value : int64_t of the timedelta value neg : bool if the a negative value """ if neg: return -value return value cdef inline timedelta_from_spec(object number, object frac, object unit): """ Parameters ---------- number : a list of number digits frac : a list of frac digits unit : a list of unit characters """ cdef: str n unit = ''.join(unit) if unit in ["M", "Y", "y"]: warnings.warn( "Units 'M', 'Y' and 'y' do not represent unambiguous " "timedelta values and will be removed in a future version.", FutureWarning, stacklevel=find_stack_level(), ) if unit == 'M': # To parse ISO 8601 string, 'M' should be treated as minute, # not month unit = 'm' unit = parse_timedelta_unit(unit) n = ''.join(number) + '.' + ''.join(frac) return cast_from_unit(float(n), unit) cpdef inline str parse_timedelta_unit(str unit): """ Parameters ---------- unit : str or None Returns ------- str Canonical unit string. Raises ------ ValueError : on non-parseable input """ if unit is None: return "ns" elif unit == "M": return unit try: return timedelta_abbrevs[unit.lower()] except KeyError: raise ValueError(f"invalid unit abbreviation: {unit}") # ---------------------------------------------------------------------- # Timedelta ops utilities cdef bint _validate_ops_compat(other): # return True if we are compat with operating if checknull_with_nat(other): return True elif is_any_td_scalar(other): return True elif isinstance(other, str): return True return False def _op_unary_method(func, name): def f(self): new_value = func(self.value) return _timedelta_from_value_and_reso(Timedelta, new_value, self._reso) f.__name__ = name return f def _binary_op_method_timedeltalike(op, name): # define a binary operation that only works if the other argument is # timedelta like or an array of timedeltalike def f(self, other): if other is NaT: return NaT elif is_datetime64_object(other) or ( PyDateTime_Check(other) and not isinstance(other, ABCTimestamp) ): # this case is for a datetime object that is specifically # *not* a Timestamp, as the Timestamp case will be # handled after `_validate_ops_compat` returns False below from pandas._libs.tslibs.timestamps import Timestamp return op(self, Timestamp(other)) # We are implicitly requiring the canonical behavior to be # defined by Timestamp methods. elif is_array(other): if other.ndim == 0: # see also: item_from_zerodim item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) return f(self, item) elif other.dtype.kind in ['m', 'M']: return op(self.to_timedelta64(), other) elif other.dtype.kind == 'O': return np.array([op(self, x) for x in other]) else: return NotImplemented elif not _validate_ops_compat(other): # Includes any of our non-cython classes return NotImplemented try: other = Timedelta(other) except ValueError: # failed to parse as timedelta return NotImplemented if other is NaT: # e.g. if original other was timedelta64('NaT') return NaT # We allow silent casting to the lower resolution if and only # if it is lossless. try: if self._reso < other._reso: other = (<_Timedelta>other)._as_reso(self._reso, round_ok=False) elif self._reso > other._reso: self = (<_Timedelta>self)._as_reso(other._reso, round_ok=False) except ValueError as err: raise ValueError( "Timedelta addition/subtraction with mismatched resolutions is not " "allowed when casting to the lower resolution would require " "lossy rounding." ) from err res = op(self.value, other.value) if res == NPY_NAT: # e.g. test_implementation_limits # TODO: more generally could do an overflowcheck in op? return NaT return _timedelta_from_value_and_reso(Timedelta, res, reso=self._reso) f.__name__ = name return f # ---------------------------------------------------------------------- # Timedelta Construction cdef inline int64_t parse_iso_format_string(str ts) except? -1: """ Extracts and cleanses the appropriate values from a match object with groups for each component of an ISO 8601 duration Parameters ---------- ts: str ISO 8601 Duration formatted string Returns ------- ns: int64_t Precision in nanoseconds of matched ISO 8601 duration Raises ------ ValueError If ``ts`` cannot be parsed """ cdef: unicode c int64_t result = 0, r int p = 0, sign = 1 object dec_unit = 'ms', err_msg bint have_dot = 0, have_value = 0, neg = 0 list number = [], unit = [] err_msg = f"Invalid ISO 8601 Duration format - {ts}" if ts[0] == "-": sign = -1 ts = ts[1:] for c in ts: # number (ascii codes) if 48 <= ord(c) <= 57: have_value = 1 if have_dot: if p == 3 and dec_unit != 'ns': unit.append(dec_unit) if dec_unit == 'ms': dec_unit = 'us' elif dec_unit == 'us': dec_unit = 'ns' p = 0 p += 1 if not len(unit): number.append(c) else: r = timedelta_from_spec(number, '0', unit) result += timedelta_as_neg(r, neg) neg = 0 unit, number = [], [c] else: if c == 'P' or c == 'T': pass # ignore marking characters P and T elif c == '-': if neg or have_value: raise ValueError(err_msg) else: neg = 1 elif c == "+": pass elif c in ['W', 'D', 'H', 'M']: if c in ['H', 'M'] and len(number) > 2: raise ValueError(err_msg) if c == 'M': c = 'min' unit.append(c) r = timedelta_from_spec(number, '0', unit) result += timedelta_as_neg(r, neg) neg = 0 unit, number = [], [] elif c == '.': # append any seconds if len(number): r = timedelta_from_spec(number, '0', 'S') result += timedelta_as_neg(r, neg) unit, number = [], [] have_dot = 1 elif c == 'S': if have_dot: # ms, us, or ns if not len(number) or p > 3: raise ValueError(err_msg) # pad to 3 digits as required pad = 3 - p while pad > 0: number.append('0') pad -= 1 r = timedelta_from_spec(number, '0', dec_unit) result += timedelta_as_neg(r, neg) else: # seconds r = timedelta_from_spec(number, '0', 'S') result += timedelta_as_neg(r, neg) else: raise ValueError(err_msg) if not have_value: # Received string only - never parsed any values raise ValueError(err_msg) return sign*result cdef _to_py_int_float(v): # Note: This used to be defined inside Timedelta.__new__ # but cython will not allow `cdef` functions to be defined dynamically. if is_integer_object(v): return int(v) elif is_float_object(v): return float(v) raise TypeError(f"Invalid type {type(v)}. Must be int or float.") def _timedelta_unpickle(value, reso): return _timedelta_from_value_and_reso(Timedelta, value, reso) cdef _timedelta_from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso): # Could make this a classmethod if/when cython supports cdef classmethods cdef: _Timedelta td_base # For millisecond and second resos, we cannot actually pass int(value) because # many cases would fall outside of the pytimedelta implementation bounds. # We pass 0 instead, and override seconds, microseconds, days. # In principle we could pass 0 for ns and us too. if reso == NPY_FR_ns: td_base = _Timedelta.__new__(cls, microseconds=int(value) // 1000) elif reso == NPY_DATETIMEUNIT.NPY_FR_us: td_base = _Timedelta.__new__(cls, microseconds=int(value)) elif reso == NPY_DATETIMEUNIT.NPY_FR_ms: td_base = _Timedelta.__new__(cls, milliseconds=0) elif reso == NPY_DATETIMEUNIT.NPY_FR_s: td_base = _Timedelta.__new__(cls, seconds=0) # Other resolutions are disabled but could potentially be implemented here: # elif reso == NPY_DATETIMEUNIT.NPY_FR_m: # td_base = _Timedelta.__new__(Timedelta, minutes=int(value)) # elif reso == NPY_DATETIMEUNIT.NPY_FR_h: # td_base = _Timedelta.__new__(Timedelta, hours=int(value)) # elif reso == NPY_DATETIMEUNIT.NPY_FR_D: # td_base = _Timedelta.__new__(Timedelta, days=int(value)) else: raise NotImplementedError( "Only resolutions 's', 'ms', 'us', 'ns' are supported." ) td_base.value = value td_base._is_populated = 0 td_base._reso = reso return td_base class MinMaxReso: """ We need to define min/max/resolution on both the Timedelta _instance_ and Timedelta class. On an instance, these depend on the object's _reso. On the class, we default to the values we would get with nanosecond _reso. """ def __init__(self, name): self._name = name def __get__(self, obj, type=None): if self._name == "min": val = np.iinfo(np.int64).min + 1 elif self._name == "max": val = np.iinfo(np.int64).max else: assert self._name == "resolution" val = 1 if obj is None: # i.e. this is on the class, default to nanos return Timedelta(val) else: return Timedelta._from_value_and_reso(val, obj._reso) def __set__(self, obj, value): raise AttributeError(f"{self._name} is not settable.") # Similar to Timestamp/datetime, this is a construction requirement for # timedeltas that we need to do object instantiation in python. This will # serve as a C extension type that shadows the Python class, where we do any # heavy lifting. cdef class _Timedelta(timedelta): # cdef readonly: # int64_t value # nanoseconds # bint _is_populated # are my components populated # int64_t _d, _h, _m, _s, _ms, _us, _ns # NPY_DATETIMEUNIT _reso # higher than np.ndarray and np.matrix __array_priority__ = 100 min = MinMaxReso("min") max = MinMaxReso("max") resolution = MinMaxReso("resolution") @property def days(self) -> int: # TODO(cython3): make cdef property # NB: using the python C-API PyDateTime_DELTA_GET_DAYS will fail # (or be incorrect) self._ensure_components() return self._d @property def seconds(self) -> int: # TODO(cython3): make cdef property # NB: using the python C-API PyDateTime_DELTA_GET_SECONDS will fail # (or be incorrect) self._ensure_components() return self._h * 3600 + self._m * 60 + self._s @property def microseconds(self) -> int: # TODO(cython3): make cdef property # NB: using the python C-API PyDateTime_DELTA_GET_MICROSECONDS will fail # (or be incorrect) self._ensure_components() return self._ms * 1000 + self._us def total_seconds(self) -> float: """Total seconds in the duration.""" # We need to override bc we overrided days/seconds/microseconds # TODO: add nanos/1e9? return self.days * 24 * 3600 + self.seconds + self.microseconds / 1_000_000 @property def freq(self) -> None: """ Freq property. .. deprecated:: 1.5.0 This argument is deprecated. """ # GH#46430 warnings.warn( "Timedelta.freq is deprecated and will be removed in a future version", FutureWarning, stacklevel=find_stack_level(), ) return None @property def is_populated(self) -> bool: """ Is_populated property. .. deprecated:: 1.5.0 This argument is deprecated. """ # GH#46430 warnings.warn( "Timedelta.is_populated is deprecated and will be removed in a future version", FutureWarning, stacklevel=find_stack_level(), ) return self._is_populated def __hash__(_Timedelta self): if self._has_ns(): # Note: this does *not* satisfy the invariance # td1 == td2 \\Rightarrow hash(td1) == hash(td2) # if td1 and td2 have different _resos. timedelta64 also has this # non-invariant behavior. # see GH#44504 return hash(self.value) else: return timedelta.__hash__(self) def __richcmp__(_Timedelta self, object other, int op): cdef: _Timedelta ots if isinstance(other, _Timedelta): ots = other elif is_any_td_scalar(other): ots = Timedelta(other) # TODO: watch out for overflows elif other is NaT: return op == Py_NE elif util.is_array(other): if other.dtype.kind == "m": return PyObject_RichCompare(self.asm8, other, op) elif other.dtype.kind == "O": # operate element-wise return np.array( [PyObject_RichCompare(self, x, op) for x in other], dtype=bool, ) if op == Py_EQ: return np.zeros(other.shape, dtype=bool) elif op == Py_NE: return np.ones(other.shape, dtype=bool) return NotImplemented # let other raise TypeError else: return NotImplemented if self._reso == ots._reso: return cmp_scalar(self.value, ots.value, op) return self._compare_mismatched_resos(ots, op) # TODO: re-use/share with Timestamp cdef inline bint _compare_mismatched_resos(self, _Timedelta other, op): # Can't just dispatch to numpy as they silently overflow and get it wrong cdef: npy_datetimestruct dts_self npy_datetimestruct dts_other # dispatch to the datetimestruct utils instead of writing new ones! pandas_datetime_to_datetimestruct(self.value, self._reso, &dts_self) pandas_datetime_to_datetimestruct(other.value, other._reso, &dts_other) return cmp_dtstructs(&dts_self, &dts_other, op) cdef bint _has_ns(self): if self._reso == NPY_FR_ns: return self.value % 1000 != 0 elif self._reso < NPY_FR_ns: # i.e. seconds, millisecond, microsecond return False else: raise NotImplementedError(self._reso) cdef _ensure_components(_Timedelta self): """ compute the components """ if self._is_populated: return cdef: pandas_timedeltastruct tds pandas_timedelta_to_timedeltastruct(self.value, self._reso, &tds) self._d = tds.days self._h = tds.hrs self._m = tds.min self._s = tds.sec self._ms = tds.ms self._us = tds.us self._ns = tds.ns self._seconds = tds.seconds self._microseconds = tds.microseconds self._is_populated = 1 cpdef timedelta to_pytimedelta(_Timedelta self): """ Convert a pandas Timedelta object into a python ``datetime.timedelta`` object. Timedelta objects are internally saved as numpy datetime64[ns] dtype. Use to_pytimedelta() to convert to object dtype. Returns ------- datetime.timedelta or numpy.array of datetime.timedelta See Also -------- to_timedelta : Convert argument to Timedelta type. Notes ----- Any nanosecond resolution will be lost. """ if self._reso == NPY_FR_ns: return timedelta(microseconds=int(self.value) / 1000) # TODO(@WillAyd): is this the right way to use components? self._ensure_components() return timedelta( days=self._d, seconds=self._seconds, microseconds=self._microseconds ) def to_timedelta64(self) -> np.timedelta64: """ Return a numpy.timedelta64 object with 'ns' precision. """ cdef: str abbrev = npy_unit_to_abbrev(self._reso) # TODO: way to create a np.timedelta64 obj with the reso directly # instead of having to get the abbrev? return np.timedelta64(self.value, abbrev) def to_numpy(self, dtype=None, copy=False) -> np.timedelta64: """ Convert the Timedelta to a NumPy timedelta64. .. versionadded:: 0.25.0 This is an alias method for `Timedelta.to_timedelta64()`. The dtype and copy parameters are available here only for compatibility. Their values will not affect the return value. Returns ------- numpy.timedelta64 See Also -------- Series.to_numpy : Similar method for Series. """ if dtype is not None or copy is not False: raise ValueError( "Timedelta.to_numpy dtype and copy arguments are ignored" ) return self.to_timedelta64() def view(self, dtype): """ Array view compatibility. """ return np.timedelta64(self.value).view(dtype) @property def components(self): """ Return a components namedtuple-like. """ self._ensure_components() # return the named tuple return Components(self._d, self._h, self._m, self._s, self._ms, self._us, self._ns) @property def delta(self): """ Return the timedelta in nanoseconds (ns), for internal compatibility. .. deprecated:: 1.5.0 This argument is deprecated. Returns ------- int Timedelta in nanoseconds. Examples -------- >>> td = pd.Timedelta('1 days 42 ns') >>> td.delta 86400000000042 >>> td = pd.Timedelta('3 s') >>> td.delta 3000000000 >>> td = pd.Timedelta('3 ms 5 us') >>> td.delta 3005000 >>> td = pd.Timedelta(42, unit='ns') >>> td.delta 42 """ # Deprecated GH#46476 warnings.warn( "Timedelta.delta is deprecated and will be removed in a future version.", FutureWarning, stacklevel=find_stack_level(), ) return self.value @property def asm8(self) -> np.timedelta64: """ Return a numpy timedelta64 array scalar view. Provides access to the array scalar view (i.e. a combination of the value and the units) associated with the numpy.timedelta64().view(), including a 64-bit integer representation of the timedelta in nanoseconds (Python int compatible). Returns ------- numpy timedelta64 array scalar view Array scalar view of the timedelta in nanoseconds. Examples -------- >>> td = pd.Timedelta('1 days 2 min 3 us 42 ns') >>> td.asm8 numpy.timedelta64(86520000003042,'ns') >>> td = pd.Timedelta('2 min 3 s') >>> td.asm8 numpy.timedelta64(123000000000,'ns') >>> td = pd.Timedelta('3 ms 5 us') >>> td.asm8 numpy.timedelta64(3005000,'ns') >>> td = pd.Timedelta(42, unit='ns') >>> td.asm8 numpy.timedelta64(42,'ns') """ return self.to_timedelta64() @property def resolution_string(self) -> str: """ Return a string representing the lowest timedelta resolution. Each timedelta has a defined resolution that represents the lowest OR most granular level of precision. Each level of resolution is represented by a short string as defined below: Resolution: Return value * Days: 'D' * Hours: 'H' * Minutes: 'T' * Seconds: 'S' * Milliseconds: 'L' * Microseconds: 'U' * Nanoseconds: 'N' Returns ------- str Timedelta resolution. Examples -------- >>> td = pd.Timedelta('1 days 2 min 3 us 42 ns') >>> td.resolution_string 'N' >>> td = pd.Timedelta('1 days 2 min 3 us') >>> td.resolution_string 'U' >>> td = pd.Timedelta('2 min 3 s') >>> td.resolution_string 'S' >>> td = pd.Timedelta(36, unit='us') >>> td.resolution_string 'U' """ self._ensure_components() if self._ns: return "N" elif self._us: return "U" elif self._ms: return "L" elif self._s: return "S" elif self._m: return "T" elif self._h: return "H" else: return "D" @property def nanoseconds(self): """ Return the number of nanoseconds (n), where 0 <= n < 1 microsecond. Returns ------- int Number of nanoseconds. See Also -------- Timedelta.components : Return all attributes with assigned values (i.e. days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). Examples -------- **Using string input** >>> td = pd.Timedelta('1 days 2 min 3 us 42 ns') >>> td.nanoseconds 42 **Using integer input** >>> td = pd.Timedelta(42, unit='ns') >>> td.nanoseconds 42 """ self._ensure_components() return self._ns def _repr_base(self, format=None) -> str: """ Parameters ---------- format : None|all|sub_day|long Returns ------- converted : string of a Timedelta """ cdef: str sign, fmt dict comp_dict object subs self._ensure_components() if self._d < 0: sign = " +" else: sign = " " if format == 'all': fmt = ("{days} days{sign}{hours:02}:{minutes:02}:{seconds:02}." "{milliseconds:03}{microseconds:03}{nanoseconds:03}") else: # if we have a partial day subs = (self._h or self._m or self._s or self._ms or self._us or self._ns) if self._ms or self._us or self._ns: seconds_fmt = "{seconds:02}.{milliseconds:03}{microseconds:03}" if self._ns: # GH#9309 seconds_fmt += "{nanoseconds:03}" else: seconds_fmt = "{seconds:02}" if format == 'sub_day' and not self._d: fmt = "{hours:02}:{minutes:02}:" + seconds_fmt elif subs or format == 'long': fmt = "{days} days{sign}{hours:02}:{minutes:02}:" + seconds_fmt else: fmt = "{days} days" comp_dict = self.components._asdict() comp_dict['sign'] = sign return fmt.format(**comp_dict) def __repr__(self) -> str: repr_based = self._repr_base(format='long') return f"Timedelta('{repr_based}')" def __str__(self) -> str: return self._repr_base(format='long') def __bool__(self) -> bool: return self.value != 0 def isoformat(self) -> str: """ Format the Timedelta as ISO 8601 Duration. ``P[n]Y[n]M[n]DT[n]H[n]M[n]S``, where the ``[n]`` s are replaced by the values. See https://en.wikipedia.org/wiki/ISO_8601#Durations. Returns ------- str See Also -------- Timestamp.isoformat : Function is used to convert the given Timestamp object into the ISO format. Notes ----- The longest component is days, whose value may be larger than 365. Every component is always included, even if its value is 0. Pandas uses nanosecond precision, so up to 9 decimal places may be included in the seconds component. Trailing 0's are removed from the seconds component after the decimal. We do not 0 pad components, so it's `...T5H...`, not `...T05H...` Examples -------- >>> td = pd.Timedelta(days=6, minutes=50, seconds=3, ... milliseconds=10, microseconds=10, nanoseconds=12) >>> td.isoformat() 'P6DT0H50M3.010010012S' >>> pd.Timedelta(hours=1, seconds=10).isoformat() 'P0DT1H0M10S' >>> pd.Timedelta(days=500.5).isoformat() 'P500DT12H0M0S' """ components = self.components seconds = (f'{components.seconds}.' f'{components.milliseconds:0>3}' f'{components.microseconds:0>3}' f'{components.nanoseconds:0>3}') # Trim unnecessary 0s, 1.000000000 -> 1 seconds = seconds.rstrip('0').rstrip('.') tpl = (f'P{components.days}DT{components.hours}' f'H{components.minutes}M{seconds}S') return tpl # ---------------------------------------------------------------- # Constructors @classmethod def _from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso): # exposing as classmethod for testing return _timedelta_from_value_and_reso(cls, value, reso) def _as_unit(self, str unit, bint round_ok=True): dtype = np.dtype(f"m8[{unit}]") reso = get_unit_from_dtype(dtype) try: return self._as_reso(reso, round_ok=round_ok) except OverflowError as err: raise OutOfBoundsTimedelta( f"Cannot cast {self} to unit='{unit}' without overflow." ) from err @cython.cdivision(False) cdef _Timedelta _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=True): cdef: int64_t value, mult, div, mod if reso == self._reso: return self value = convert_reso(self.value, self._reso, reso, round_ok=round_ok) return type(self)._from_value_and_reso(value, reso=reso) # Python front end to C extension type _Timedelta # This serves as the box for timedelta64 class Timedelta(_Timedelta): """ Represents a duration, the difference between two dates or times. Timedelta is the pandas equivalent of python's ``datetime.timedelta`` and is interchangeable with it in most cases. Parameters ---------- value : Timedelta, timedelta, np.timedelta64, str, or int unit : str, default 'ns' Denote the unit of the input, if input is an integer. Possible values: * 'W', 'D', 'T', 'S', 'L', 'U', or 'N' * 'days' or 'day' * 'hours', 'hour', 'hr', or 'h' * 'minutes', 'minute', 'min', or 'm' * 'seconds', 'second', or 'sec' * 'milliseconds', 'millisecond', 'millis', or 'milli' * 'microseconds', 'microsecond', 'micros', or 'micro' * 'nanoseconds', 'nanosecond', 'nanos', 'nano', or 'ns'. **kwargs Available kwargs: {days, seconds, microseconds, milliseconds, minutes, hours, weeks}. Values for construction in compat with datetime.timedelta. Numpy ints and floats will be coerced to python ints and floats. Notes ----- The constructor may take in either both values of value and unit or kwargs as above. Either one of them must be used during initialization The ``.value`` attribute is always in ns. If the precision is higher than nanoseconds, the precision of the duration is truncated to nanoseconds. Examples -------- Here we initialize Timedelta object with both value and unit >>> td = pd.Timedelta(1, "d") >>> td Timedelta('1 days 00:00:00') Here we initialize the Timedelta object with kwargs >>> td2 = pd.Timedelta(days=1) >>> td2 Timedelta('1 days 00:00:00') We see that either way we get the same result """ _req_any_kwargs_new = {"weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "nanoseconds"} def __new__(cls, object value=_no_input, unit=None, **kwargs): cdef _Timedelta td_base if value is _no_input: if not len(kwargs): raise ValueError("cannot construct a Timedelta without a " "value/unit or descriptive keywords " "(days,seconds....)") kwargs = {key: _to_py_int_float(kwargs[key]) for key in kwargs} unsupported_kwargs = set(kwargs) unsupported_kwargs.difference_update(cls._req_any_kwargs_new) if unsupported_kwargs or not cls._req_any_kwargs_new.intersection(kwargs): raise ValueError( "cannot construct a Timedelta from the passed arguments, " "allowed keywords are " "[weeks, days, hours, minutes, seconds, " "milliseconds, microseconds, nanoseconds]" ) # GH43764, convert any input to nanoseconds first and then # create the timestamp. This ensures that any potential # nanosecond contributions from kwargs parsed as floats # are taken into consideration. seconds = int(( ( (kwargs.get('days', 0) + kwargs.get('weeks', 0) * 7) * 24 + kwargs.get('hours', 0) ) * 3600 + kwargs.get('minutes', 0) * 60 + kwargs.get('seconds', 0) ) * 1_000_000_000 ) value = np.timedelta64( int(kwargs.get('nanoseconds', 0)) + int(kwargs.get('microseconds', 0) * 1_000) + int(kwargs.get('milliseconds', 0) * 1_000_000) + seconds ) if unit in {'Y', 'y', 'M'}: raise ValueError( "Units 'M', 'Y', and 'y' are no longer supported, as they do not " "represent unambiguous timedelta values durations." ) # GH 30543 if pd.Timedelta already passed, return it # check that only value is passed if isinstance(value, _Timedelta) and unit is None and len(kwargs) == 0: return value elif isinstance(value, _Timedelta): value = value.value elif isinstance(value, str): if unit is not None: raise ValueError("unit must not be specified if the value is a str") if (len(value) > 0 and value[0] == 'P') or ( len(value) > 1 and value[:2] == '-P' ): value = parse_iso_format_string(value) else: value = parse_timedelta_string(value) value = np.timedelta64(value) elif PyDelta_Check(value): value = convert_to_timedelta64(value, 'ns') elif is_timedelta64_object(value): value = ensure_td64ns(value) elif is_tick_object(value): value = np.timedelta64(value.nanos, 'ns') elif is_integer_object(value) or is_float_object(value): # unit=None is de-facto 'ns' unit = parse_timedelta_unit(unit) value = convert_to_timedelta64(value, unit) elif checknull_with_nat(value): return NaT else: raise ValueError( "Value must be Timedelta, string, integer, " f"float, timedelta or convertible, not {type(value).__name__}" ) if is_timedelta64_object(value): value = value.view('i8') # nat if value == NPY_NAT: return NaT return _timedelta_from_value_and_reso(cls, value, NPY_FR_ns) def __setstate__(self, state): if len(state) == 1: # older pickle, only supported nanosecond value = state[0] reso = NPY_FR_ns else: value, reso = state self.value = value self._reso = reso def __reduce__(self): object_state = self.value, self._reso return (_timedelta_unpickle, object_state) @cython.cdivision(True) def _round(self, freq, mode): cdef: int64_t result, unit, remainder ndarray[int64_t] arr from pandas._libs.tslibs.offsets import to_offset to_offset(freq).nanos # raises on non-fixed freq unit = delta_to_nanoseconds(to_offset(freq), self._reso) arr = np.array([self.value], dtype="i8") result = round_nsint64(arr, mode, unit)[0] return Timedelta._from_value_and_reso(result, self._reso) def round(self, freq): """ Round the Timedelta to the specified resolution. Parameters ---------- freq : str Frequency string indicating the rounding resolution. Returns ------- a new Timedelta rounded to the given resolution of `freq` Raises ------ ValueError if the freq cannot be converted """ return self._round(freq, RoundTo.NEAREST_HALF_EVEN) def floor(self, freq): """ Return a new Timedelta floored to this resolution. Parameters ---------- freq : str Frequency string indicating the flooring resolution. """ return self._round(freq, RoundTo.MINUS_INFTY) def ceil(self, freq): """ Return a new Timedelta ceiled to this resolution. Parameters ---------- freq : str Frequency string indicating the ceiling resolution. """ return self._round(freq, RoundTo.PLUS_INFTY) # ---------------------------------------------------------------- # Arithmetic Methods # TODO: Can some of these be defined in the cython class? __neg__ = _op_unary_method(lambda x: -x, '__neg__') __pos__ = _op_unary_method(lambda x: x, '__pos__') __abs__ = _op_unary_method(lambda x: abs(x), '__abs__') __add__ = _binary_op_method_timedeltalike(lambda x, y: x + y, '__add__') __radd__ = _binary_op_method_timedeltalike(lambda x, y: x + y, '__radd__') __sub__ = _binary_op_method_timedeltalike(lambda x, y: x - y, '__sub__') __rsub__ = _binary_op_method_timedeltalike(lambda x, y: y - x, '__rsub__') def __mul__(self, other): if is_integer_object(other) or is_float_object(other): if util.is_nan(other): # np.nan * timedelta -> np.timedelta64("NaT"), in this case NaT return NaT return _timedelta_from_value_and_reso( Timedelta, (other * self.value), reso=self._reso, ) elif is_array(other): if other.ndim == 0: # see also: item_from_zerodim item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) return self.__mul__(item) return other * self.to_timedelta64() return NotImplemented __rmul__ = __mul__ def __truediv__(self, other): cdef: int64_t new_value if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) if other is NaT: return np.nan if other._reso != self._reso: raise ValueError( "division between Timedeltas with mismatched resolutions " "are not supported. Explicitly cast to matching resolutions " "before dividing." ) return self.value / float(other.value) elif is_integer_object(other) or is_float_object(other): # integers or floats if util.is_nan(other): return NaT return Timedelta._from_value_and_reso( (self.value / other), self._reso ) elif is_array(other): if other.ndim == 0: # see also: item_from_zerodim item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) return self.__truediv__(item) return self.to_timedelta64() / other return NotImplemented def __rtruediv__(self, other): if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) if other is NaT: return np.nan if self._reso != other._reso: raise ValueError( "division between Timedeltas with mismatched resolutions " "are not supported. Explicitly cast to matching resolutions " "before dividing." ) return float(other.value) / self.value elif is_array(other): if other.ndim == 0: # see also: item_from_zerodim item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) return self.__rtruediv__(item) elif other.dtype.kind == "O": # GH#31869 return np.array([x / self for x in other]) # TODO: if other.dtype.kind == "m" and other.dtype != self.asm8.dtype # then should disallow for consistency with scalar behavior; requires # deprecation cycle. (or changing scalar behavior) return other / self.to_timedelta64() return NotImplemented def __floordiv__(self, other): # numpy does not implement floordiv for timedelta64 dtype, so we cannot # just defer if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) if other is NaT: return np.nan if self._reso != other._reso: raise ValueError( "floordivision between Timedeltas with mismatched resolutions " "are not supported. Explicitly cast to matching resolutions " "before dividing." ) return self.value // other.value elif is_integer_object(other) or is_float_object(other): if util.is_nan(other): return NaT return type(self)._from_value_and_reso(self.value // other, self._reso) elif is_array(other): if other.ndim == 0: # see also: item_from_zerodim item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) return self.__floordiv__(item) if other.dtype.kind == 'm': # also timedelta-like if self._reso != NPY_FR_ns: raise NotImplementedError return _broadcast_floordiv_td64(self.value, other, _floordiv) elif other.dtype.kind in ['i', 'u', 'f']: if other.ndim == 0: return self // other.item() else: return self.to_timedelta64() // other raise TypeError(f'Invalid dtype {other.dtype} for __floordiv__') return NotImplemented def __rfloordiv__(self, other): # numpy does not implement floordiv for timedelta64 dtype, so we cannot # just defer if _should_cast_to_timedelta(other): # We interpret NaT as timedelta64("NaT") other = Timedelta(other) if other is NaT: return np.nan if self._reso != other._reso: raise ValueError( "floordivision between Timedeltas with mismatched resolutions " "are not supported. Explicitly cast to matching resolutions " "before dividing." ) return other.value // self.value elif is_array(other): if other.ndim == 0: # see also: item_from_zerodim item = cnp.PyArray_ToScalar(cnp.PyArray_DATA(other), other) return self.__rfloordiv__(item) if other.dtype.kind == 'm': # also timedelta-like if self._reso != NPY_FR_ns: raise NotImplementedError return _broadcast_floordiv_td64(self.value, other, _rfloordiv) # Includes integer array // Timedelta, disallowed in GH#19761 raise TypeError(f'Invalid dtype {other.dtype} for __floordiv__') return NotImplemented def __mod__(self, other): # Naive implementation, room for optimization return self.__divmod__(other)[1] def __rmod__(self, other): # Naive implementation, room for optimization return self.__rdivmod__(other)[1] def __divmod__(self, other): # Naive implementation, room for optimization div = self // other return div, self - div * other def __rdivmod__(self, other): # Naive implementation, room for optimization div = other // self return div, other - div * self cdef bint is_any_td_scalar(object obj): """ Cython equivalent for `isinstance(obj, (timedelta, np.timedelta64, Tick))` Parameters ---------- obj : object Returns ------- bool """ return ( PyDelta_Check(obj) or is_timedelta64_object(obj) or is_tick_object(obj) ) cdef bint _should_cast_to_timedelta(object obj): """ Should we treat this object as a Timedelta for the purpose of a binary op """ return ( is_any_td_scalar(obj) or obj is None or obj is NaT or isinstance(obj, str) ) cdef _floordiv(int64_t value, right): return value // right cdef _rfloordiv(int64_t value, right): # analogous to referencing operator.div, but there is no operator.rfloordiv return right // value cdef _broadcast_floordiv_td64( int64_t value, ndarray other, object (*operation)(int64_t value, object right) ): """ Boilerplate code shared by Timedelta.__floordiv__ and Timedelta.__rfloordiv__ because np.timedelta64 does not implement these. Parameters ---------- value : int64_t; `self.value` from a Timedelta object other : object operation : function, either _floordiv or _rfloordiv Returns ------- result : varies based on `other` """ # assumes other.dtype.kind == 'm', i.e. other is timedelta-like # assumes other.ndim != 0 # We need to watch out for np.timedelta64('NaT'). mask = other.view('i8') == NPY_NAT res = operation(value, other.astype('m8[ns]', copy=False).astype('i8')) if mask.any(): res = res.astype('f8') res[mask] = np.nan return res