Skip to content

Commit 94e2194

Browse files
committed
BUG: Arithmetic, timezone and offsets operations affecting to NaT
1 parent 8161c85 commit 94e2194

File tree

7 files changed

+189
-15
lines changed

7 files changed

+189
-15
lines changed

doc/source/release.rst

+2
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,8 @@ Bug Fixes
370370
- Better error message when passing a frequency of 'MS' in ``Period`` construction (GH5332)
371371
- Bug in `Series.__unicode__` when `max_rows` is `None` and the Series has more than 1000 rows. (:issue:`6863`)
372372
- Bug in ``groupby.get_group`` where a datetlike wasn't always accepted (:issue:`5267`)
373+
- Bug in ``DatetimeIndex.tz_localize`` and ``DatetimeIndex.tz_convert`` affects to NaT (:issue:`5546`)
374+
- Bug in arithmetic operations affecting to NaT (:issue:`6873`)
373375

374376
pandas 0.13.1
375377
-------------

pandas/tseries/index.py

+3
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,10 @@ def __sub__(self, other):
611611
def _add_delta(self, delta):
612612
if isinstance(delta, (Tick, timedelta)):
613613
inc = offsets._delta_to_nanoseconds(delta)
614+
mask = self.asi8 == tslib.iNaT
614615
new_values = (self.asi8 + inc).view(_NS_DTYPE)
616+
new_values[mask] = tslib.iNaT
617+
new_values = new_values.view(_NS_DTYPE)
615618
elif isinstance(delta, np.timedelta64):
616619
new_values = self.to_series() + delta
617620
else:

pandas/tseries/offsets.py

+33
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
from pandas import _np_version_under1p7
1515

16+
import functools
17+
1618
__all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay',
1719
'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd',
1820
'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd',
@@ -35,6 +37,15 @@ def as_datetime(obj):
3537
obj = f()
3638
return obj
3739

40+
def apply_nat(func):
41+
@functools.wraps(func)
42+
def wrapper(self, other):
43+
if other is tslib.NaT:
44+
return tslib.NaT
45+
else:
46+
return func(self, other)
47+
return wrapper
48+
3849
#----------------------------------------------------------------------
3950
# DateOffset
4051

@@ -102,6 +113,7 @@ def __init__(self, n=1, **kwds):
102113
else:
103114
self._offset = timedelta(1)
104115

116+
@apply_nat
105117
def apply(self, other):
106118
other = as_datetime(other)
107119
if len(self.kwds) > 0:
@@ -382,6 +394,7 @@ def get_str(td):
382394
def isAnchored(self):
383395
return (self.n == 1)
384396

397+
@apply_nat
385398
def apply(self, other):
386399
if isinstance(other, datetime):
387400
n = self.n
@@ -502,6 +515,7 @@ def __setstate__(self, state):
502515
self.__dict__ = state
503516
self._set_busdaycalendar()
504517

518+
@apply_nat
505519
def apply(self, other):
506520
if self.n <= 0:
507521
roll = 'forward'
@@ -582,6 +596,7 @@ def name(self):
582596
class MonthEnd(MonthOffset):
583597
"""DateOffset of one month end"""
584598

599+
@apply_nat
585600
def apply(self, other):
586601
other = datetime(other.year, other.month, other.day,
587602
tzinfo=other.tzinfo)
@@ -606,6 +621,7 @@ def onOffset(cls, dt):
606621
class MonthBegin(MonthOffset):
607622
"""DateOffset of one month at beginning"""
608623

624+
@apply_nat
609625
def apply(self, other):
610626
n = self.n
611627

@@ -628,6 +644,7 @@ class BusinessMonthEnd(MonthOffset):
628644
def isAnchored(self):
629645
return (self.n == 1)
630646

647+
@apply_nat
631648
def apply(self, other):
632649
other = datetime(other.year, other.month, other.day)
633650

@@ -653,6 +670,7 @@ def apply(self, other):
653670
class BusinessMonthBegin(MonthOffset):
654671
"""DateOffset of one business month at beginning"""
655672

673+
@apply_nat
656674
def apply(self, other):
657675
n = self.n
658676

@@ -710,6 +728,7 @@ def __init__(self, n=1, **kwds):
710728
def isAnchored(self):
711729
return (self.n == 1 and self.weekday is not None)
712730

731+
@apply_nat
713732
def apply(self, other):
714733
if self.weekday is None:
715734
return as_timestamp(as_datetime(other) + self.n * self._inc)
@@ -811,6 +830,7 @@ def __init__(self, n=1, **kwds):
811830

812831
self.kwds = kwds
813832

833+
@apply_nat
814834
def apply(self, other):
815835
offsetOfMonth = self.getOffsetOfMonth(other)
816836

@@ -890,6 +910,7 @@ def __init__(self, n=1, **kwds):
890910

891911
self.kwds = kwds
892912

913+
@apply_nat
893914
def apply(self, other):
894915
offsetOfMonth = self.getOffsetOfMonth(other)
895916

@@ -983,6 +1004,7 @@ class BQuarterEnd(QuarterOffset):
9831004
_from_name_startingMonth = 12
9841005
_prefix = 'BQ'
9851006

1007+
@apply_nat
9861008
def apply(self, other):
9871009
n = self.n
9881010

@@ -1037,6 +1059,7 @@ class BQuarterBegin(QuarterOffset):
10371059
_from_name_startingMonth = 1
10381060
_prefix = 'BQS'
10391061

1062+
@apply_nat
10401063
def apply(self, other):
10411064
n = self.n
10421065
other = as_datetime(other)
@@ -1086,6 +1109,7 @@ def __init__(self, n=1, **kwds):
10861109
def isAnchored(self):
10871110
return (self.n == 1 and self.startingMonth is not None)
10881111

1112+
@apply_nat
10891113
def apply(self, other):
10901114
n = self.n
10911115
other = as_datetime(other)
@@ -1117,6 +1141,7 @@ class QuarterBegin(QuarterOffset):
11171141
def isAnchored(self):
11181142
return (self.n == 1 and self.startingMonth is not None)
11191143

1144+
@apply_nat
11201145
def apply(self, other):
11211146
n = self.n
11221147
other = as_datetime(other)
@@ -1166,6 +1191,7 @@ class BYearEnd(YearOffset):
11661191
_default_month = 12
11671192
_prefix = 'BA'
11681193

1194+
@apply_nat
11691195
def apply(self, other):
11701196
n = self.n
11711197
other = as_datetime(other)
@@ -1203,6 +1229,7 @@ class BYearBegin(YearOffset):
12031229
_default_month = 1
12041230
_prefix = 'BAS'
12051231

1232+
@apply_nat
12061233
def apply(self, other):
12071234
n = self.n
12081235
other = as_datetime(other)
@@ -1234,6 +1261,7 @@ class YearEnd(YearOffset):
12341261
_default_month = 12
12351262
_prefix = 'A'
12361263

1264+
@apply_nat
12371265
def apply(self, other):
12381266
def _increment(date):
12391267
if date.month == self.month:
@@ -1290,6 +1318,7 @@ class YearBegin(YearOffset):
12901318
_default_month = 1
12911319
_prefix = 'AS'
12921320

1321+
@apply_nat
12931322
def apply(self, other):
12941323
def _increment(date):
12951324
year = date.year
@@ -1410,6 +1439,7 @@ def onOffset(self, dt):
14101439
else:
14111440
return year_end == dt
14121441

1442+
@apply_nat
14131443
def apply(self, other):
14141444
n = self.n
14151445
prev_year = self.get_year_end(
@@ -1596,6 +1626,7 @@ def __init__(self, n=1, **kwds):
15961626
def isAnchored(self):
15971627
return self.n == 1 and self._offset.isAnchored()
15981628

1629+
@apply_nat
15991630
def apply(self, other):
16001631
other = as_datetime(other)
16011632
n = self.n
@@ -1693,6 +1724,7 @@ class Easter(DateOffset):
16931724
def __init__(self, n=1, **kwds):
16941725
super(Easter, self).__init__(n, **kwds)
16951726

1727+
@apply_nat
16961728
def apply(self, other):
16971729

16981730
currentEaster = easter(other.year)
@@ -1786,6 +1818,7 @@ def delta(self):
17861818
def nanos(self):
17871819
return _delta_to_nanoseconds(self.delta)
17881820

1821+
@apply_nat
17891822
def apply(self, other):
17901823
if type(other) == date:
17911824
other = datetime(other.year, other.month, other.day)

pandas/tseries/tests/test_offsets.py

+40-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import nose
66
from nose.tools import assert_raises
77

8+
89
import numpy as np
910

1011
from pandas.core.datetools import (
@@ -20,7 +21,7 @@
2021
from pandas.tseries.tools import parse_time_string
2122
import pandas.tseries.offsets as offsets
2223

23-
from pandas.tslib import monthrange, OutOfBoundsDatetime
24+
from pandas.tslib import monthrange, OutOfBoundsDatetime, NaT
2425
from pandas.lib import Timestamp
2526
from pandas.util.testing import assertRaisesRegexp
2627
import pandas.util.testing as tm
@@ -98,14 +99,33 @@ def test_to_m8():
9899
class TestBase(tm.TestCase):
99100
_offset = None
100101

102+
offset_types = [getattr(offsets, o) for o in offsets.__all__]
103+
skip_np_u1p7 = [offsets.CustomBusinessDay, offsets.CDay, offsets.Nano]
104+
105+
def _get_offset(self, klass, value=1):
106+
# create instance from offset class
107+
if klass is FY5253 or klass is FY5253Quarter:
108+
klass = klass(n=value, startingMonth=1, weekday=1,
109+
qtr_with_extra_week=1, variation='last')
110+
elif klass is WeekOfMonth or klass is LastWeekOfMonth:
111+
klass = LastWeekOfMonth(n=value, weekday=5)
112+
else:
113+
try:
114+
klass = klass(value)
115+
except:
116+
klass = klass()
117+
return klass
118+
101119
def test_apply_out_of_range(self):
102120
if self._offset is None:
103121
raise nose.SkipTest("_offset not defined to test out-of-range")
122+
if self._offset in self.skip_np_u1p7:
123+
raise nose.SkipTest('numpy >= 1.7 required')
104124

105125
# try to create an out-of-bounds result timestamp; if we can't create the offset
106126
# skip
107127
try:
108-
offset = self._offset(10000)
128+
offset = self._get_offset(self._offset, value=10000)
109129

110130
result = Timestamp('20080101') + offset
111131
self.assertIsInstance(result, datetime)
@@ -114,16 +134,27 @@ def test_apply_out_of_range(self):
114134
except (ValueError, KeyError):
115135
raise nose.SkipTest("cannot create out_of_range offset")
116136

137+
138+
class TestOps(TestBase):
139+
117140
def test_return_type(self):
141+
for offset in self.offset_types:
142+
if _np_version_under1p7 and offset in self.skip_np_u1p7:
143+
continue
118144

119-
# make sure that we are returning a Timestamp
120-
try:
121-
offset = self._offset(1)
122-
except:
123-
raise nose.SkipTest("_offset not defined to test return_type")
145+
offset = self._get_offset(offset)
146+
147+
# make sure that we are returning a Timestamp
148+
result = Timestamp('20080101') + offset
149+
self.assertIsInstance(result, Timestamp)
150+
151+
# make sure that we are returning NaT
152+
self.assert_(NaT + offset is NaT)
153+
self.assert_(offset + NaT is NaT)
154+
155+
self.assert_(NaT - offset is NaT)
156+
self.assert_((-offset).apply(NaT) is NaT)
124157

125-
result = Timestamp('20080101') + offset
126-
self.assertIsInstance(result, Timestamp)
127158

128159
class TestDateOffset(TestBase):
129160
_multiprocess_can_split_ = True

pandas/tseries/tests/test_timezones.py

+35
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,41 @@ def test_tzaware_offset(self):
975975
offset = dates + timedelta(hours=5)
976976
self.assert_(offset.equals(expected))
977977

978+
def test_nat(self):
979+
# GH 5546
980+
dates = [NaT]
981+
idx = DatetimeIndex(dates)
982+
idx = idx.tz_localize('US/Pacific')
983+
self.assert_(idx.equals(DatetimeIndex(dates, tz='US/Pacific')))
984+
idx = idx.tz_convert('US/Eastern')
985+
self.assert_(idx.equals(DatetimeIndex(dates, tz='US/Eastern')))
986+
idx = idx.tz_convert('UTC')
987+
self.assert_(idx.equals(DatetimeIndex(dates, tz='UTC')))
988+
989+
dates = ['2010-12-01 00:00', '2010-12-02 00:00', NaT]
990+
idx = DatetimeIndex(dates)
991+
idx = idx.tz_localize('US/Pacific')
992+
self.assert_(idx.equals(DatetimeIndex(dates, tz='US/Pacific')))
993+
idx = idx.tz_convert('US/Eastern')
994+
expected = ['2010-12-01 03:00', '2010-12-02 03:00', NaT]
995+
self.assert_(idx.equals(DatetimeIndex(expected, tz='US/Eastern')))
996+
997+
idx = idx + offsets.Hour(5)
998+
expected = ['2010-12-01 08:00', '2010-12-02 08:00', NaT]
999+
self.assert_(idx.equals(DatetimeIndex(expected, tz='US/Eastern')))
1000+
idx = idx.tz_convert('US/Pacific')
1001+
expected = ['2010-12-01 05:00', '2010-12-02 05:00', NaT]
1002+
self.assert_(idx.equals(DatetimeIndex(expected, tz='US/Pacific')))
1003+
1004+
if not _np_version_under1p7:
1005+
idx = idx + np.timedelta64(3, 'h')
1006+
expected = ['2010-12-01 08:00', '2010-12-02 08:00', NaT]
1007+
self.assert_(idx.equals(DatetimeIndex(expected, tz='US/Pacific')))
1008+
1009+
idx = idx.tz_convert('US/Eastern')
1010+
expected = ['2010-12-01 11:00', '2010-12-02 11:00', NaT]
1011+
self.assert_(idx.equals(DatetimeIndex(expected, tz='US/Eastern')))
1012+
9781013

9791014
if __name__ == '__main__':
9801015
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],

pandas/tseries/tests/test_tslib.py

+41
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,47 @@ def test_nanosecond_string_parsing(self):
269269
self.timestamp = Timestamp('2013-05-01 07:15:45.123456789')
270270
self.assertEqual(self.timestamp.value, 1367392545123456000)
271271

272+
def test_nat_arithmetic(self):
273+
# GH 6873
274+
nat = tslib.NaT
275+
t = Timestamp('2014-01-01')
276+
dt = datetime.datetime(2014, 1, 1)
277+
delta = datetime.timedelta(3600)
278+
279+
# Timestamp / datetime
280+
for (left, right) in [(nat, nat), (nat, t), (dt, nat)]:
281+
# NaT + Timestamp-like should raise TypeError
282+
with tm.assertRaises(TypeError):
283+
left + right
284+
with tm.assertRaises(TypeError):
285+
right + left
286+
287+
# NaT - Timestamp-like (or inverse) returns NaT
288+
self.assert_((left - right) is tslib.NaT)
289+
self.assert_((right - left) is tslib.NaT)
290+
291+
# timedelta-like
292+
# offsets are tested in test_offsets.py
293+
for (left, right) in [(nat, delta)]:
294+
# NaT + timedelta-like returns NaT
295+
self.assert_((left + right) is tslib.NaT)
296+
# timedelta-like + NaT should raise TypeError
297+
with tm.assertRaises(TypeError):
298+
right + left
299+
300+
self.assert_((left - right) is tslib.NaT)
301+
with tm.assertRaises(TypeError):
302+
right - left
303+
304+
if _np_version_under1p7:
305+
self.assertEqual(nat + np.timedelta64(1, 'h'), tslib.NaT)
306+
with tm.assertRaises(TypeError):
307+
np.timedelta64(1, 'h') + nat
308+
309+
self.assertEqual(nat - np.timedelta64(1, 'h'), tslib.NaT)
310+
with tm.assertRaises(TypeError):
311+
np.timedelta64(1, 'h') - nat
312+
272313

273314
class TestTslib(tm.TestCase):
274315

0 commit comments

Comments
 (0)