Skip to content

Commit f4c9d96

Browse files
jbrockmendeljreback
authored andcommitted
handle NaT add/sub in one place (#19903)
1 parent 74dbfd0 commit f4c9d96

File tree

4 files changed

+52
-50
lines changed

4 files changed

+52
-50
lines changed

pandas/core/indexes/datetimelike.py

+41-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# -*- coding: utf-8 -*-
12
"""
23
Base and utility classes for tseries type pandas objects.
34
"""
@@ -640,6 +641,28 @@ def _add_datelike(self, other):
640641
def _sub_datelike(self, other):
641642
raise com.AbstractMethodError(self)
642643

644+
def _add_nat(self):
645+
"""Add pd.NaT to self"""
646+
if is_period_dtype(self):
647+
raise TypeError('Cannot add {cls} and {typ}'
648+
.format(cls=type(self).__name__,
649+
typ=type(NaT).__name__))
650+
651+
# GH#19124 pd.NaT is treated like a timedelta for both timedelta
652+
# and datetime dtypes
653+
return self._nat_new(box=True)
654+
655+
def _sub_nat(self):
656+
"""Subtract pd.NaT from self"""
657+
# GH#19124 Timedelta - datetime is not in general well-defined.
658+
# We make an exception for pd.NaT, which in this case quacks
659+
# like a timedelta.
660+
# For datetime64 dtypes by convention we treat NaT as a datetime, so
661+
# this subtraction returns a timedelta64 dtype.
662+
# For period dtype, timedelta64 is a close-enough return dtype.
663+
result = self._nat_new(box=False)
664+
return result.view('timedelta64[ns]')
665+
643666
def _sub_period(self, other):
644667
return NotImplemented
645668

@@ -686,6 +709,8 @@ def __add__(self, other):
686709
return NotImplemented
687710

688711
# scalar others
712+
elif other is NaT:
713+
result = self._add_nat()
689714
elif isinstance(other, (DateOffset, timedelta, np.timedelta64)):
690715
result = self._add_delta(other)
691716
elif isinstance(other, (datetime, np.datetime64)):
@@ -711,9 +736,13 @@ def __add__(self, other):
711736
else: # pragma: no cover
712737
return NotImplemented
713738

714-
if result is not NotImplemented:
715-
res_name = ops.get_op_result_name(self, other)
716-
result.name = res_name
739+
if result is NotImplemented:
740+
return NotImplemented
741+
elif not isinstance(result, Index):
742+
# Index.__new__ will choose appropriate subclass for dtype
743+
result = Index(result)
744+
res_name = ops.get_op_result_name(self, other)
745+
result.name = res_name
717746
return result
718747

719748
cls.__add__ = __add__
@@ -731,6 +760,8 @@ def __sub__(self, other):
731760
return NotImplemented
732761

733762
# scalar others
763+
elif other is NaT:
764+
result = self._sub_nat()
734765
elif isinstance(other, (DateOffset, timedelta, np.timedelta64)):
735766
result = self._add_delta(-other)
736767
elif isinstance(other, (datetime, np.datetime64)):
@@ -762,9 +793,13 @@ def __sub__(self, other):
762793
else: # pragma: no cover
763794
return NotImplemented
764795

765-
if result is not NotImplemented:
766-
res_name = ops.get_op_result_name(self, other)
767-
result.name = res_name
796+
if result is NotImplemented:
797+
return NotImplemented
798+
elif not isinstance(result, Index):
799+
# Index.__new__ will choose appropriate subclass for dtype
800+
result = Index(result)
801+
res_name = ops.get_op_result_name(self, other)
802+
result.name = res_name
768803
return result
769804

770805
cls.__sub__ = __sub__

pandas/core/indexes/datetimes.py

+5-15
Original file line numberDiff line numberDiff line change
@@ -853,32 +853,22 @@ def __setstate__(self, state):
853853
raise Exception("invalid pickle state")
854854
_unpickle_compat = __setstate__
855855

856-
def _add_datelike(self, other):
857-
# adding a timedeltaindex to a datetimelike
858-
if other is libts.NaT:
859-
return self._nat_new(box=True)
860-
raise TypeError("cannot add {0} and {1}"
861-
.format(type(self).__name__,
862-
type(other).__name__))
863-
864856
def _sub_datelike(self, other):
865-
# subtract a datetime from myself, yielding a TimedeltaIndex
866-
from pandas import TimedeltaIndex
867-
857+
# subtract a datetime from myself, yielding a ndarray[timedelta64[ns]]
868858
if isinstance(other, (DatetimeIndex, np.ndarray)):
869859
# if other is an ndarray, we assume it is datetime64-dtype
870860
other = DatetimeIndex(other)
871-
872861
# require tz compat
873862
if not self._has_same_tz(other):
874863
raise TypeError("{cls} subtraction must have the same "
875864
"timezones or no timezones"
876865
.format(cls=type(self).__name__))
877866
result = self._sub_datelike_dti(other)
878867
elif isinstance(other, (datetime, np.datetime64)):
868+
assert other is not libts.NaT
879869
other = Timestamp(other)
880870
if other is libts.NaT:
881-
result = self._nat_new(box=False)
871+
return self - libts.NaT
882872
# require tz compat
883873
elif not self._has_same_tz(other):
884874
raise TypeError("Timestamp subtraction must have the same "
@@ -893,7 +883,7 @@ def _sub_datelike(self, other):
893883
raise TypeError("cannot subtract {cls} and {typ}"
894884
.format(cls=type(self).__name__,
895885
typ=type(other).__name__))
896-
return TimedeltaIndex(result)
886+
return result.view('timedelta64[ns]')
897887

898888
def _sub_datelike_dti(self, other):
899889
"""subtraction of two DatetimeIndexes"""
@@ -906,7 +896,7 @@ def _sub_datelike_dti(self, other):
906896
if self.hasnans or other.hasnans:
907897
mask = (self._isnan) | (other._isnan)
908898
new_values[mask] = libts.iNaT
909-
return new_values.view('i8')
899+
return new_values.view('timedelta64[ns]')
910900

911901
def _maybe_update_attributes(self, attrs):
912902
""" Update Index attributes (e.g. freq) depending on op """

pandas/core/indexes/period.py

+1-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
is_scalar,
1313
is_datetime64_dtype,
1414
is_datetime64_any_dtype,
15-
is_timedelta64_dtype,
1615
is_period_dtype,
1716
is_bool_dtype,
1817
pandas_dtype,
@@ -23,7 +22,6 @@
2322
import pandas.tseries.frequencies as frequencies
2423
from pandas.tseries.frequencies import get_freq_code as _gfc
2524
from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index
26-
from pandas.core.indexes.timedeltas import TimedeltaIndex
2725
from pandas.core.indexes.datetimelike import DatelikeOps, DatetimeIndexOpsMixin
2826
from pandas.core.tools.datetimes import parse_time_string
2927
import pandas.tseries.offsets as offsets
@@ -700,16 +698,6 @@ def _maybe_convert_timedelta(self, other):
700698
return other.n
701699
msg = _DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
702700
raise IncompatibleFrequency(msg)
703-
elif isinstance(other, np.ndarray):
704-
if is_integer_dtype(other):
705-
return other
706-
elif is_timedelta64_dtype(other):
707-
offset = frequencies.to_offset(self.freq)
708-
if isinstance(offset, offsets.Tick):
709-
nanos = delta_to_nanoseconds(other)
710-
offset_nanos = delta_to_nanoseconds(offset)
711-
if (nanos % offset_nanos).all() == 0:
712-
return nanos // offset_nanos
713701
elif is_integer(other):
714702
# integer is passed to .shift via
715703
# _add_datetimelike_methods basically
@@ -724,10 +712,7 @@ def _add_delta(self, other):
724712
return self.shift(ordinal_delta)
725713

726714
def _sub_datelike(self, other):
727-
if other is tslib.NaT:
728-
new_data = np.empty(len(self), dtype=np.int64)
729-
new_data.fill(tslib.iNaT)
730-
return TimedeltaIndex(new_data)
715+
assert other is not tslib.NaT
731716
return NotImplemented
732717

733718
def _sub_period(self, other):

pandas/core/indexes/timedeltas.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -414,16 +414,13 @@ def _evaluate_with_timedelta_like(self, other, op):
414414
def _add_datelike(self, other):
415415
# adding a timedeltaindex to a datetimelike
416416
from pandas import Timestamp, DatetimeIndex
417-
418-
if other is NaT:
419-
# GH#19124 pd.NaT is treated like a timedelta
420-
return self._nat_new()
421-
elif isinstance(other, (DatetimeIndex, np.ndarray)):
417+
if isinstance(other, (DatetimeIndex, np.ndarray)):
422418
# if other is an ndarray, we assume it is datetime64-dtype
423419
# defer to implementation in DatetimeIndex
424420
other = DatetimeIndex(other)
425421
return other + self
426422
else:
423+
assert other is not NaT
427424
other = Timestamp(other)
428425
i8 = self.asi8
429426
result = checked_add_with_arr(i8, other.value,
@@ -432,14 +429,9 @@ def _add_datelike(self, other):
432429
return DatetimeIndex(result)
433430

434431
def _sub_datelike(self, other):
435-
# GH#19124 Timedelta - datetime is not in general well-defined.
436-
# We make an exception for pd.NaT, which in this case quacks
437-
# like a timedelta.
438-
if other is NaT:
439-
return self._nat_new()
440-
else:
441-
raise TypeError("cannot subtract a datelike from a {cls}"
442-
.format(cls=type(self).__name__))
432+
assert other is not NaT
433+
raise TypeError("cannot subtract a datelike from a {cls}"
434+
.format(cls=type(self).__name__))
443435

444436
def _addsub_offset_array(self, other, op):
445437
# Add or subtract Array-like of DateOffset objects

0 commit comments

Comments
 (0)