Skip to content

Commit d00e9c1

Browse files
committed
Merge pull request #8981 from jreback/timestamp
BUG: Bug in Timestamp-Timestamp not returning a Timedelta type (GH8865)
2 parents 06e44dd + c6fda69 commit d00e9c1

File tree

7 files changed

+125
-12
lines changed

7 files changed

+125
-12
lines changed

doc/source/whatsnew/v0.15.2.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ Experimental
9292

9393
Bug Fixes
9494
~~~~~~~~~
95+
96+
- Bug in Timestamp-Timestamp not returning a Timedelta type and datelike-datelike ops with timezones (:issue:`8865`)
97+
- Made consistent a timezone mismatch exception (either tz operated with None or incompatible timezone), will now return ``TypeError`` rather than ``ValueError`` (a couple of edge cases only), (:issue:`8865`)
98+
- Report a ``TypeError`` when invalid/no paramaters are passed in a groupby (:issue:`8015`)
99+
- Bug in packaging pandas with ``py2app/cx_Freeze`` (:issue:`8602`, :issue:`8831`)
100+
- Bug in ``groupby`` signatures that didn't include \*args or \*\*kwargs (:issue:`8733`).
101+
- ``io.data.Options`` now raises ``RemoteDataError`` when no expiry dates are available from Yahoo and when it receives no data from Yahoo (:issue:`8761`), (:issue:`8783`).
102+
- Unclear error message in csv parsing when passing dtype and names and the parsed data is a different data type (:issue:`8833`)
103+
- Bug in slicing a multi-index with an empty list and at least one boolean indexer (:issue:`8781`)
104+
- ``io.data.Options`` now raises ``RemoteDataError`` when no expiry dates are available from Yahoo (:issue:`8761`).
105+
- ``Timedelta`` kwargs may now be numpy ints and floats (:issue:`8757`).
106+
- Fixed several outstanding bugs for ``Timedelta`` arithmetic and comparisons (:issue:`8813`, :issue:`5963`, :issue:`5436`).
107+
- ``sql_schema`` now generates dialect appropriate ``CREATE TABLE`` statements (:issue:`8697`)
108+
- ``slice`` string method now takes step into account (:issue:`8754`)
109+
- Bug in ``BlockManager`` where setting values with different type would break block integrity (:issue:`8850`)
110+
- Bug in ``DatetimeIndex`` when using ``time`` object as key (:issue:`8667`)
111+
- Bug in ``merge`` where ``how='left'`` and ``sort=False`` would not preserve left frame order (:issue:`7331`)
112+
95113
- Fix negative step support for label-based slices (:issue:`8753`)
96114

97115
Old behavior:

pandas/tests/test_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ def test_ops(self):
285285
expected = pd.Period(ordinal=getattr(o.values, op)(), freq=o.freq)
286286
try:
287287
self.assertEqual(result, expected)
288-
except ValueError:
289-
# comparing tz-aware series with np.array results in ValueError
288+
except TypeError:
289+
# comparing tz-aware series with np.array results in TypeError
290290
expected = expected.astype('M8[ns]').astype('int64')
291291
self.assertEqual(result.value, expected)
292292

pandas/tseries/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ def __sub__(self, other):
346346
cls.__sub__ = __sub__
347347

348348
def __rsub__(self, other):
349-
return -self + other
349+
return -(self - other)
350350
cls.__rsub__ = __rsub__
351351

352352
cls.__iadd__ = __add__

pandas/tseries/index.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ def _generate(cls, start, end, periods, name, offset,
381381
try:
382382
inferred_tz = tools._infer_tzinfo(start, end)
383383
except:
384-
raise ValueError('Start and end cannot both be tz-aware with '
384+
raise TypeError('Start and end cannot both be tz-aware with '
385385
'different timezones')
386386

387387
inferred_tz = tslib.maybe_get_tz(inferred_tz)
@@ -645,6 +645,11 @@ def _sub_datelike(self, other):
645645

646646
from pandas import TimedeltaIndex
647647
other = Timestamp(other)
648+
649+
# require tz compat
650+
if tslib.get_timezone(self.tz) != tslib.get_timezone(other.tzinfo):
651+
raise TypeError("Timestamp subtraction must have the same timezones or no timezones")
652+
648653
i8 = self.asi8
649654
result = i8 - other.value
650655
result = self._maybe_mask_results(result,fill_value=tslib.iNaT)

pandas/tseries/tests/test_base.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,74 @@ def test_subtraction_ops(self):
468468
expected = DatetimeIndex(['20121231',pd.NaT,'20121230'])
469469
tm.assert_index_equal(result,expected)
470470

471+
def test_subtraction_ops_with_tz(self):
472+
473+
# check that dt/dti subtraction ops with tz are validated
474+
dti = date_range('20130101',periods=3)
475+
ts = Timestamp('20130101')
476+
dt = ts.to_datetime()
477+
dti_tz = date_range('20130101',periods=3).tz_localize('US/Eastern')
478+
ts_tz = Timestamp('20130101').tz_localize('US/Eastern')
479+
ts_tz2 = Timestamp('20130101').tz_localize('CET')
480+
dt_tz = ts_tz.to_datetime()
481+
td = Timedelta('1 days')
482+
483+
def _check(result, expected):
484+
self.assertEqual(result,expected)
485+
self.assertIsInstance(result, Timedelta)
486+
487+
# scalars
488+
result = ts - ts
489+
expected = Timedelta('0 days')
490+
_check(result, expected)
491+
492+
result = dt_tz - ts_tz
493+
expected = Timedelta('0 days')
494+
_check(result, expected)
495+
496+
result = ts_tz - dt_tz
497+
expected = Timedelta('0 days')
498+
_check(result, expected)
499+
500+
# tz mismatches
501+
self.assertRaises(TypeError, lambda : dt_tz - ts)
502+
self.assertRaises(TypeError, lambda : dt_tz - dt)
503+
self.assertRaises(TypeError, lambda : dt_tz - ts_tz2)
504+
self.assertRaises(TypeError, lambda : dt - dt_tz)
505+
self.assertRaises(TypeError, lambda : ts - dt_tz)
506+
self.assertRaises(TypeError, lambda : ts_tz2 - ts)
507+
self.assertRaises(TypeError, lambda : ts_tz2 - dt)
508+
self.assertRaises(TypeError, lambda : ts_tz - ts_tz2)
509+
510+
# with dti
511+
self.assertRaises(TypeError, lambda : dti - ts_tz)
512+
self.assertRaises(TypeError, lambda : dti_tz - ts)
513+
self.assertRaises(TypeError, lambda : dti_tz - ts_tz2)
514+
515+
result = dti_tz-dt_tz
516+
expected = TimedeltaIndex(['0 days','1 days','2 days'])
517+
tm.assert_index_equal(result,expected)
518+
519+
result = dt_tz-dti_tz
520+
expected = TimedeltaIndex(['0 days','-1 days','-2 days'])
521+
tm.assert_index_equal(result,expected)
522+
523+
result = dti_tz-ts_tz
524+
expected = TimedeltaIndex(['0 days','1 days','2 days'])
525+
tm.assert_index_equal(result,expected)
526+
527+
result = ts_tz-dti_tz
528+
expected = TimedeltaIndex(['0 days','-1 days','-2 days'])
529+
tm.assert_index_equal(result,expected)
530+
531+
result = td - td
532+
expected = Timedelta('0 days')
533+
_check(result, expected)
534+
535+
result = dti_tz - td
536+
expected = DatetimeIndex(['20121231','20130101','20130102'],tz='US/Eastern')
537+
tm.assert_index_equal(result,expected)
538+
471539
def test_dti_tdi_numeric_ops(self):
472540

473541
# These are normally union/diff set-like ops

pandas/tseries/tests/test_tslib.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pandas import tslib
66
import datetime
77

8-
from pandas.core.api import Timestamp, Series
8+
from pandas.core.api import Timestamp, Series, Timedelta
99
from pandas.tslib import period_asfreq, period_ordinal, get_timezone
1010
from pandas.tseries.index import date_range
1111
from pandas.tseries.frequencies import get_freq
@@ -232,13 +232,13 @@ def test_tz(self):
232232
conv = local.tz_convert('US/Eastern')
233233
self.assertEqual(conv.nanosecond, 5)
234234
self.assertEqual(conv.hour, 19)
235-
235+
236236
def test_tz_localize_ambiguous(self):
237-
237+
238238
ts = Timestamp('2014-11-02 01:00')
239239
ts_dst = ts.tz_localize('US/Eastern', ambiguous=True)
240240
ts_no_dst = ts.tz_localize('US/Eastern', ambiguous=False)
241-
241+
242242
rng = date_range('2014-11-02', periods=3, freq='H', tz='US/Eastern')
243243
self.assertEqual(rng[1], ts_dst)
244244
self.assertEqual(rng[2], ts_no_dst)
@@ -679,8 +679,8 @@ def test_addition_subtraction_types(self):
679679
self.assertEqual(type(timestamp_instance - 1), Timestamp)
680680

681681
# Timestamp + datetime not supported, though subtraction is supported and yields timedelta
682-
self.assertEqual(type(timestamp_instance - datetime_instance), datetime.timedelta)
683-
682+
# more tests in tseries/base/tests/test_base.py
683+
self.assertEqual(type(timestamp_instance - datetime_instance), Timedelta)
684684
self.assertEqual(type(timestamp_instance + timedelta_instance), Timestamp)
685685
self.assertEqual(type(timestamp_instance - timedelta_instance), Timestamp)
686686

pandas/tslib.pyx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,10 @@ cdef class _Timestamp(datetime):
812812
object other) except -1:
813813
if self.tzinfo is None:
814814
if other.tzinfo is not None:
815-
raise ValueError('Cannot compare tz-naive and tz-aware '
815+
raise TypeError('Cannot compare tz-naive and tz-aware '
816816
'timestamps')
817817
elif other.tzinfo is None:
818-
raise ValueError('Cannot compare tz-naive and tz-aware timestamps')
818+
raise TypeError('Cannot compare tz-naive and tz-aware timestamps')
819819

820820
cpdef datetime to_datetime(_Timestamp self):
821821
cdef:
@@ -865,6 +865,11 @@ cdef class _Timestamp(datetime):
865865

866866
# a Timestamp-DatetimeIndex -> yields a negative TimedeltaIndex
867867
elif getattr(other,'_typ',None) == 'datetimeindex':
868+
869+
# we may be passed reverse ops
870+
if get_timezone(getattr(self,'tzinfo',None)) != get_timezone(other.tz):
871+
raise TypeError("Timestamp subtraction must have the same timezones or no timezones")
872+
868873
return -other.__sub__(self)
869874

870875
# a Timestamp-TimedeltaIndex -> yields a negative TimedeltaIndex
@@ -873,6 +878,23 @@ cdef class _Timestamp(datetime):
873878

874879
elif other is NaT:
875880
return NaT
881+
882+
# coerce if necessary if we are a Timestamp-like
883+
if isinstance(self, datetime) and (isinstance(other, datetime) or is_datetime64_object(other)):
884+
self = Timestamp(self)
885+
other = Timestamp(other)
886+
887+
# validate tz's
888+
if get_timezone(self.tzinfo) != get_timezone(other.tzinfo):
889+
raise TypeError("Timestamp subtraction must have the same timezones or no timezones")
890+
891+
# scalar Timestamp/datetime - Timestamp/datetime -> yields a Timedelta
892+
try:
893+
return Timedelta(self.value-other.value)
894+
except (OverflowError, OutOfBoundsDatetime):
895+
pass
896+
897+
# scalar Timestamp/datetime - Timedelta -> yields a Timestamp (with same timezone if specified)
876898
return datetime.__sub__(self, other)
877899

878900
cpdef _get_field(self, field):

0 commit comments

Comments
 (0)