Skip to content

WIP: NaTD #24645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 280 additions & 2 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import division

import collections
import textwrap
import warnings
Expand Down Expand Up @@ -36,6 +38,7 @@ from pandas._libs.tslibs.nattype import nat_strings
from pandas._libs.tslibs.nattype cimport (
checknull_with_nat, NPY_NAT, c_NaT as NaT)
from pandas._libs.tslibs.offsets cimport to_offset
from pandas._libs.tslibs.offsets import _Tick

# ----------------------------------------------------------------------
# Constants
Expand Down Expand Up @@ -1323,7 +1326,7 @@ class Timedelta(_Timedelta):
# integers or floats
return Timedelta(self.value / other, unit='ns')

elif not _validate_ops_compat(other):
elif not _validate_ops_compat(other) or other is NaTD:
return NotImplemented

other = Timedelta(other)
Expand All @@ -1346,7 +1349,7 @@ class Timedelta(_Timedelta):
elif hasattr(other, 'dtype'):
return other / self.to_timedelta64()

elif not _validate_ops_compat(other):
elif not _validate_ops_compat(other) or other is NaTD:
return NotImplemented

other = Timedelta(other)
Expand Down Expand Up @@ -1522,3 +1525,278 @@ cdef _broadcast_floordiv_td64(int64_t value, object other,
# resolution in ns
Timedelta.min = Timedelta(np.iinfo(np.int64).min + 1)
Timedelta.max = Timedelta(np.iinfo(np.int64).max)


# ----------------------------------------------------------------------
# An internally used timedelta-NaT largely to use in place of
# np.timedelta64('NaT') for binary operations.

cdef inline bint is_tdlike_scalar(object obj):
return (is_timedelta64_object(obj) or
PyDelta_Check(obj) or
isinstance(obj, _Tick))


cdef class _TDNaTType(timedelta):
"""
NaTD (Not-a-TimeDelta) is a pandas-internal analogue of NaT that behaves
explicitly like a timedelta, never like a datetime.
"""
cdef readonly:
int64_t value

def __cinit__(self):
# TODO: is there such a thing as a super __cinit__?
self.value = NPY_NAT

def __repr__(self):
return 'NaTD'

def __str__(self):
return 'NaTD'

@property
def asm8(self):
return np.timedelta64('NaT', 'ns')

def __hash__(_TDNaTType self):
# py3k needs this defined here
return hash(self.value)

def __richcmp__(_TDNaTType self, object other, int op):
cdef:
int ndim = getattr(other, 'ndim', -1)

if ndim == -1:
return op == Py_NE

if ndim == 0:
if is_tdlike_scalar(other):
return op == Py_NE
else: # FIXME: shouldnt we be raising only for inequalities?
raise TypeError('Cannot compare type %r with type %r' %
(type(self).__name__, type(other).__name__))
# Note: instead of passing "other, self, _reverse_ops[op]", we observe
# that `_nat_scalar_rules` is invariant under `_reverse_ops`,
# rendering it unnecessary.
return PyObject_RichCompare(other, self, op)

def __add__(self, other):
if other is NaT:
return NaT
if is_tdlike_scalar(other):
return NaTD

if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if util.is_array(other):
if other.dtype.kind in ['m', 'M']:
result = np.empty(other.shape, dtype='i8')
result.fill(NPY_NAT)
return result.view(other.dtype)
elif other.dtype.kind == 'O':
return np.array([self + x for x in other])

raise TypeError("Cannot add dtype {dtype} to Timedelta"
.format(dtype=other.dtype))

elif hasattr(other, "dtype"):
return NotImplemented

# all thats left is invalid scalars
raise TypeError("Cannot add {typ} to Timedelta"
.format(typ=type(other).__name__))

def __sub__(self, other):
if is_tdlike_scalar(other):
return NaTD

if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if util.is_array(other):
if other.dtype.kind == 'm':
result = np.empty(other.shape, dtype='i8')
result.fill(NPY_NAT)
return result.view(other.dtype)
elif other.dtype.kind == 'O':
# TODO: does this get shape right?
return np.array([self - x for x in other])

raise TypeError("Cannot subtract dtype {dtype} from Timedelta"
.format(dtype=other.dtype))

elif hasattr(other, "dtype"):
return NotImplemented

# all thats left is invalid scalars
raise TypeError("Cannot subtract {typ} from Timedelta"
.format(typ=type(other).__name__))

def __mul__(self, other):
if is_integer_object(other) or is_float_object(other):
return NaTD

if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if util.is_array(other):
if other.dtype.kind in ['i', 'u', 'f']:
result = np.empty(other.shape, dtype='i8')
result.fill(NPY_NAT)
return result.view("timedelta64[ns]")
elif other.dtype.kind == 'O':
# TODO: does this get shape right?
return np.array([self * x for x in other])

raise TypeError("Cannot multiply Timedelta by dtype {dtype}"
.format(dtype=other.dtype))

elif hasattr(other, "dtype"):
return NotImplemented

# all thats left is invalid scalars
raise TypeError("Cannot multiply Timedelta by {typ}"
.format(typ=type(other).__name__))

def __truediv__(self, other):
if is_tdlike_scalar(other):
return np.nan

if is_integer_object(other) or is_float_object(other):
return NaTD

if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if util.is_array(other):
if other.dtype.kind in ['i', 'u', 'f']:
result = np.empty(other.shape, dtype='i8')
result.fill(NPY_NAT)
return result.view("timedelta64[ns]")
elif other.dtype.kind == 'm':
result = np.empty(other.shape, dtype=np.float64)
result.fill(np.nan)
return result
elif other.dtype.kind == 'O':
# TODO: does this get shape right?
return np.array([self / x for x in other])

raise TypeError("Cannot divide Timedelta by dtype {dtype}"
.format(dtype=other.dtype))

elif hasattr(other, "dtype"):
return NotImplemented

# all thats left is invalid scalars
raise TypeError("Cannot divide Timedelta by {typ}"
.format(typ=type(other).__name__))

def __floordiv__(self, other):
return self.__truediv__(other) # TODO: is this right?

def __mod__(self, other):
# Naive implementation, room for optimization
return self.__divmod__(other)[1]

def __divmod__(self, other):
# Naive implementation, room for optimization
div = self // other
return div, self - div * other

if not PY3:
def __div__(self, other):
return self.__truediv__(other)


class TDNaTType(_TDNaTType):
__array_priority__ = 100

def __radd__(self, other):
return self.__add__(other)

def __rsub__(self, other):
if is_tdlike_scalar(other):
return NaTD

if is_datetime64_object(other) or PyDateTime_Check(other):
return NaT

if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if util.is_array(other):
if other.dtype.kind == 'M':
result = np.empty(other.shape, dtype='i8')
result.fill(NPY_NAT)
return result.view(other.dtype)
if other.dtype.kind == 'm':
result = np.empty(other.shape, dtype='i8')
result.fill(NPY_NAT)
return result.view(other.dtype)
elif other.dtype.kind == 'O':
return np.array([x - self for x in other])

raise TypeError("Cannot subtract Timedelta from dtype {dtype}"
.format(dtype=other.dtype))

elif hasattr(other, "dtype"):
return NotImplemented

# all thats left is invalid scalars
raise TypeError("Cannot subtract Timedelta from {typ}"
.format(typ=type(other).__name__))

def __rmul__(self, other):
return self.__mul__(other)

def __rtruediv__(self, other):
if is_tdlike_scalar(other):
return np.nan

if hasattr(other, '_typ'):
# Series, DataFrame, ...
return NotImplemented

if util.is_array(other):
if other.dtype.kind == 'm':
result = np.empty(other.shape, dtype=np.float64)
result.fill(np.nan)
return result
elif other.dtype.kind == 'O':
return np.array([x / self for x in other])

raise TypeError("Cannot divide dtype {dtype} by Timedelta"
.format(dtype=other.dtype))

elif hasattr(other, "dtype"):
return NotImplemented

# all thats left is invalid scalars
raise TypeError("Cannot divide {typ} by Timedelta"
.format(typ=type(other).__name__))

def __rfloordiv__(self, other):
return self.__rtruediv__(other) # TODO: is this right?

def __rmod__(self, other):
# Naive implementation, room for optimization
return self.__rdivmod__(other)[1]

def __rdivmod__(self, other):
# Naive implementation, room for optimization
div = other // self
return div, other - div * self

if not PY3:
def __rdiv__(self, other):
return self.__rtruediv__(other)


NaTD = TDNaTType()
15 changes: 6 additions & 9 deletions pandas/core/arrays/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pandas._libs.tslibs import NaT, Timedelta, Timestamp, iNaT
from pandas._libs.tslibs.fields import get_timedelta_field
from pandas._libs.tslibs.timedeltas import (
array_to_timedelta64, parse_timedelta_unit)
NaTD, array_to_timedelta64, parse_timedelta_unit)
import pandas.compat as compat
from pandas.util._decorators import Appender

Expand Down Expand Up @@ -364,7 +364,7 @@ def _add_datetimelike_scalar(self, other):

assert other is not NaT
other = Timestamp(other)
if other is NaT:
if other is NaT: # TODO: use NaTD
# In this case we specifically interpret NaT as a datetime, not
# the timedelta interpretation we would get by returning self + NaT
result = self.asi8.view('m8[ms]') + NaT.to_datetime64()
Expand Down Expand Up @@ -435,7 +435,7 @@ def __truediv__(self, other):

if isinstance(other, (timedelta, np.timedelta64, Tick)):
other = Timedelta(other)
if other is NaT:
if other is NaT: # TODO: use NaTD
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we actually do this (which is easy to implement, but I didn't because we don't have test coverage yet) then a bunch of this code can be templated/de-duplicated.

# specifically timedelta64-NaT
result = np.empty(self.shape, dtype=np.float64)
result.fill(np.nan)
Expand Down Expand Up @@ -485,7 +485,7 @@ def __rtruediv__(self, other):

if isinstance(other, (timedelta, np.timedelta64, Tick)):
other = Timedelta(other)
if other is NaT:
if other is NaT: # TODO: use NaTD
# specifically timedelta64-NaT
result = np.empty(self.shape, dtype=np.float64)
result.fill(np.nan)
Expand Down Expand Up @@ -534,7 +534,7 @@ def __floordiv__(self, other):
if is_scalar(other):
if isinstance(other, (timedelta, np.timedelta64, Tick)):
other = Timedelta(other)
if other is NaT:
if other is NaT: # TODO: use NaTD
# treat this specifically as timedelta-NaT
result = np.empty(self.shape, dtype=np.float64)
result.fill(np.nan)
Expand Down Expand Up @@ -598,10 +598,7 @@ def __rfloordiv__(self, other):
if isinstance(other, (timedelta, np.timedelta64, Tick)):
other = Timedelta(other)
if other is NaT:
# treat this specifically as timedelta-NaT
result = np.empty(self.shape, dtype=np.float64)
result.fill(np.nan)
return result
other = NaTD

# dispatch to Timedelta implementation
result = other.__floordiv__(self._data)
Expand Down
8 changes: 6 additions & 2 deletions pandas/core/indexes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np

from pandas._libs import (
Timedelta, algos as libalgos, index as libindex, join as libjoin, lib,
NaT, Timedelta, algos as libalgos, index as libindex, join as libjoin, lib,
tslibs)
from pandas._libs.lib import is_datetime_array
import pandas.compat as compat
Expand Down Expand Up @@ -4888,6 +4888,10 @@ def _evaluate_with_timedelta_like(self, other, op):
other=type(other).__name__))

other = Timedelta(other)
if other is NaT:
from pandas._libs.tslibs.timedeltas import NaTD
other = NaTD

values = self.values

with np.errstate(all='ignore'):
Expand Down Expand Up @@ -5027,8 +5031,8 @@ def _add_numeric_methods_binary(cls):
cls.__div__ = _make_arithmetic_op(operator.div, cls)
cls.__rdiv__ = _make_arithmetic_op(ops.rdiv, cls)

# TODO: rmod? rdivmod?
cls.__mod__ = _make_arithmetic_op(operator.mod, cls)
cls.__rmod__ = _make_arithmetic_op(ops.rmod, cls)
cls.__floordiv__ = _make_arithmetic_op(operator.floordiv, cls)
cls.__rfloordiv__ = _make_arithmetic_op(ops.rfloordiv, cls)
cls.__divmod__ = _make_arithmetic_op(divmod, cls)
Expand Down
Loading