Skip to content

Commit 30e937f

Browse files
committed
BUG: cannot subtract Timestamp with different timezones (pandas-dev#31793)
1 parent 7ca1aa8 commit 30e937f

File tree

6 files changed

+87
-31
lines changed

6 files changed

+87
-31
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ Timezones
311311
^^^^^^^^^
312312
- Bug in different ``tzinfo`` objects representing UTC not being treated as equivalent (:issue:`39216`)
313313
- Bug in ``dateutil.tz.gettz("UTC")`` not being recognized as equivalent to other UTC-representing tzinfos (:issue:`39276`)
314+
- Bug in ``Timestamp`` and ``DatetimeIndex`` incorrectly raising a ``TypeError`` when subtracting two times that don't have the same timezone offset (:issue:`31793`)
314315
-
315316

316317
Numeric

pandas/_libs/tslibs/timestamps.pyx

+4-4
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,10 @@ cdef class _Timestamp(ABCTimestamp):
319319
else:
320320
self = type(other)(self)
321321

322-
# validate tz's
323-
if not tz_compare(self.tzinfo, other.tzinfo):
324-
raise TypeError("Timestamp subtraction must have the "
325-
"same timezones or no timezones")
322+
if (self.tzinfo is None) ^ (other.tzinfo is None):
323+
raise TypeError(
324+
"Cannot compare tz-naive and tz-aware datetime-like objects."
325+
)
326326

327327
# scalar Timestamp/datetime - Timestamp/datetime -> yields a
328328
# Timedelta

pandas/core/arrays/datetimes.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -685,12 +685,7 @@ def _sub_datetime_arraylike(self, other):
685685
assert is_datetime64_dtype(other)
686686
other = type(self)(other)
687687

688-
if not self._has_same_tz(other):
689-
# require tz compat
690-
raise TypeError(
691-
f"{type(self).__name__} subtraction must have the same "
692-
"timezones or no timezones"
693-
)
688+
self._assert_tzawareness_compat(other)
694689

695690
self_i8 = self.asi8
696691
other_i8 = other.asi8
@@ -734,11 +729,7 @@ def _sub_datetimelike_scalar(self, other):
734729
if other is NaT:
735730
return self - NaT
736731

737-
if not self._has_same_tz(other):
738-
# require tz compat
739-
raise TypeError(
740-
"Timestamp subtraction must have the same timezones or no timezones"
741-
)
732+
self._assert_tzawareness_compat(other)
742733

743734
i8 = self.asi8
744735
result = checked_add_with_arr(i8, -other.value, arr_mask=self._isnan)

pandas/tests/arithmetic/test_datetime64.py

+42-6
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,46 @@ def test_dt64arr_isub_timedeltalike_scalar(
864864
rng -= two_hours
865865
tm.assert_equal(rng, expected)
866866

867+
def test_dt64_array_sub_dt_with_different_timezone(self, box_with_array):
868+
t1 = pd.date_range("20130101", periods=3).tz_localize("US/Eastern")
869+
t1 = tm.box_expected(t1, box_with_array)
870+
t2 = Timestamp("20130101").tz_localize("CET")
871+
872+
result = t1 - t2
873+
expected = TimedeltaIndex(
874+
["0 days 06:00:00", "1 days 06:00:00", "2 days 06:00:00"]
875+
)
876+
expected = tm.box_expected(expected, box_with_array)
877+
tm.assert_equal(result, expected)
878+
879+
result = t2 - t1
880+
expected = TimedeltaIndex(
881+
["-1 days +18:00:00", "-2 days +18:00:00", "-3 days +18:00:00"]
882+
)
883+
expected = tm.box_expected(expected, box_with_array)
884+
tm.assert_equal(result, expected)
885+
886+
def test_dt64_array_sub_dt64_array_with_different_timezone(self, box_with_array):
887+
t1 = pd.date_range("20130101", periods=3).tz_localize("US/Eastern")
888+
t1 = tm.box_expected(t1, box_with_array)
889+
890+
t2 = pd.date_range("20130101", periods=3).tz_localize("CET")
891+
t2 = tm.box_expected(t2, box_with_array)
892+
893+
result = t1 - t2
894+
expected = TimedeltaIndex(
895+
["0 days 06:00:00", "0 days 06:00:00", "0 days 06:00:00"]
896+
)
897+
expected = tm.box_expected(expected, box_with_array)
898+
tm.assert_equal(result, expected)
899+
900+
result = t2 - t1
901+
expected = TimedeltaIndex(
902+
["-1 days +18:00:00", "-1 days +18:00:00", "-1 days +18:00:00"]
903+
)
904+
expected = tm.box_expected(expected, box_with_array)
905+
tm.assert_equal(result, expected)
906+
867907
# TODO: redundant with test_dt64arr_add_timedeltalike_scalar
868908
def test_dt64arr_add_td64_scalar(self, box_with_array):
869909
# scalar timedeltas/np.timedelta64 objects
@@ -1045,7 +1085,7 @@ def test_dt64arr_aware_sub_dt64ndarray_raises(
10451085
dt64vals = dti.values
10461086

10471087
dtarr = tm.box_expected(dti, box_with_array)
1048-
msg = "subtraction must have the same timezones or"
1088+
msg = "Cannot compare tz-naive and tz-aware datetime"
10491089
with pytest.raises(TypeError, match=msg):
10501090
dtarr - dt64vals
10511091
with pytest.raises(TypeError, match=msg):
@@ -2234,24 +2274,20 @@ def test_sub_dti_dti(self):
22342274

22352275
dti = date_range("20130101", periods=3)
22362276
dti_tz = date_range("20130101", periods=3).tz_localize("US/Eastern")
2237-
dti_tz2 = date_range("20130101", periods=3).tz_localize("UTC")
22382277
expected = TimedeltaIndex([0, 0, 0])
22392278

22402279
result = dti - dti
22412280
tm.assert_index_equal(result, expected)
22422281

22432282
result = dti_tz - dti_tz
22442283
tm.assert_index_equal(result, expected)
2245-
msg = "DatetimeArray subtraction must have the same timezones or"
2284+
msg = "Cannot compare tz-naive and tz-aware datetime-like objects"
22462285
with pytest.raises(TypeError, match=msg):
22472286
dti_tz - dti
22482287

22492288
with pytest.raises(TypeError, match=msg):
22502289
dti - dti_tz
22512290

2252-
with pytest.raises(TypeError, match=msg):
2253-
dti_tz - dti_tz2
2254-
22552291
# isub
22562292
dti -= dti
22572293
tm.assert_index_equal(dti, expected)

pandas/tests/arithmetic/test_timedelta64.py

+5-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import (
44
datetime,
55
timedelta,
6+
timezone,
67
)
78

89
import numpy as np
@@ -384,36 +385,31 @@ def _check(result, expected):
384385
expected = Timedelta("0 days")
385386
_check(result, expected)
386387

388+
# gotta chedk this
387389
# tz mismatches
388-
msg = "Timestamp subtraction must have the same timezones or no timezones"
390+
msg = "Cannot compare tz-naive and tz-aware datetime-like objects."
389391
with pytest.raises(TypeError, match=msg):
390392
dt_tz - ts
391393
msg = "can't subtract offset-naive and offset-aware datetimes"
392394
with pytest.raises(TypeError, match=msg):
393395
dt_tz - dt
394-
msg = "Timestamp subtraction must have the same timezones or no timezones"
395-
with pytest.raises(TypeError, match=msg):
396-
dt_tz - ts_tz2
397396
msg = "can't subtract offset-naive and offset-aware datetimes"
398397
with pytest.raises(TypeError, match=msg):
399398
dt - dt_tz
400-
msg = "Timestamp subtraction must have the same timezones or no timezones"
399+
msg = "Cannot compare tz-naive and tz-aware datetime-like objects."
401400
with pytest.raises(TypeError, match=msg):
402401
ts - dt_tz
403402
with pytest.raises(TypeError, match=msg):
404403
ts_tz2 - ts
405404
with pytest.raises(TypeError, match=msg):
406405
ts_tz2 - dt
407-
with pytest.raises(TypeError, match=msg):
408-
ts_tz - ts_tz2
409406

407+
msg = 'Cannot compare tz-naive and tz-aware'
410408
# with dti
411409
with pytest.raises(TypeError, match=msg):
412410
dti - ts_tz
413411
with pytest.raises(TypeError, match=msg):
414412
dti_tz - ts
415-
with pytest.raises(TypeError, match=msg):
416-
dti_tz - ts_tz2
417413

418414
result = dti_tz - dt_tz
419415
expected = TimedeltaIndex(["0 days", "1 days", "2 days"])

pandas/tests/scalar/timestamp/test_arithmetic.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import (
22
datetime,
33
timedelta,
4+
timezone,
45
)
56

67
import numpy as np
@@ -99,7 +100,7 @@ def test_rsub_dtscalars(self, tz_naive_fixture):
99100
if tz_naive_fixture is None:
100101
assert other.to_datetime64() - ts == td
101102
else:
102-
msg = "subtraction must have"
103+
msg = "Cannot subtract offset-naive and offset-aware datetimes."
103104
with pytest.raises(TypeError, match=msg):
104105
other.to_datetime64() - ts
105106

@@ -109,6 +110,37 @@ def test_timestamp_sub_datetime(self):
109110
assert (ts - dt).days == 1
110111
assert (dt - ts).days == -1
111112

113+
def test_subtract_tzaware_datetime(self):
114+
t1 = Timestamp("2020-10-22T22:00:00+00:00")
115+
t2 = datetime(2020, 10, 22, 22, tzinfo=timezone.utc)
116+
117+
result = t1 - t2
118+
119+
assert isinstance(result, Timedelta)
120+
assert result == Timedelta("0 days")
121+
122+
def test_subtract_timestamp_from_different_timezone(self):
123+
t1 = Timestamp("20130101").tz_localize("US/Eastern")
124+
t2 = Timestamp("20130101").tz_localize("CET")
125+
126+
result = t1 - t2
127+
128+
assert isinstance(result, Timedelta)
129+
assert result == Timedelta("0 days 06:00:00")
130+
131+
def test_subtracting_involving_datetime_with_different_tz(self):
132+
t1 = datetime(2013, 1, 1, tzinfo=timezone(timedelta(hours=-5)))
133+
t2 = Timestamp("20130101").tz_localize("CET")
134+
135+
result = t1 - t2
136+
137+
assert isinstance(result, Timedelta)
138+
assert result == Timedelta("0 days 06:00:00")
139+
140+
result = t2 - t1
141+
assert isinstance(result, Timedelta)
142+
assert result == Timedelta("-1 days +18:00:00")
143+
112144
def test_addition_subtraction_types(self):
113145
# Assert on the types resulting from Timestamp +/- various date/time
114146
# objects

0 commit comments

Comments
 (0)