Skip to content

Commit e1cab30

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

File tree

5 files changed

+71
-19
lines changed

5 files changed

+71
-19
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 subtract offset-naive and offset-aware datetimes."
325+
)
326326

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

pandas/core/arrays/datetimes.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -734,11 +734,9 @@ def _sub_datetimelike_scalar(self, other):
734734
if other is NaT:
735735
return self - NaT
736736

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-
)
737+
if (self.tzinfo is None) ^ (other.tzinfo is None):
738+
# require non-ambiguous timezones
739+
raise TypeError("Cannot subtract offset-naive and offset-aware datetimes.")
742740

743741
i8 = self.asi8
744742
result = checked_add_with_arr(i8, -other.value, arr_mask=self._isnan)

pandas/tests/arithmetic/test_timedelta64.py

+30-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
@@ -366,6 +367,8 @@ def test_subtraction_ops_with_tz(self):
366367
ts_tz2 = Timestamp("20130101").tz_localize("CET")
367368
dt_tz = ts_tz.to_pydatetime()
368369
td = Timedelta("1 days")
370+
ts_utc = Timestamp("2020-10-22T22:00:00+00:00")
371+
dt_utc = datetime(2020, 10, 22, 22, tzinfo=timezone.utc)
369372

370373
def _check(result, expected):
371374
assert result == expected
@@ -384,36 +387,54 @@ def _check(result, expected):
384387
expected = Timedelta("0 days")
385388
_check(result, expected)
386389

390+
result = ts_utc - dt_utc
391+
expected = Timedelta("0 days")
392+
_check(result, expected)
393+
394+
result = dt_tz - ts_tz2
395+
expected = Timedelta("0 days 06:00:00")
396+
_check(result, expected)
397+
398+
result = ts_tz2 - dt_tz
399+
expected = Timedelta("-1 days +18:00:00")
400+
_check(result, expected)
401+
402+
# gotta chedk this
387403
# tz mismatches
388-
msg = "Timestamp subtraction must have the same timezones or no timezones"
404+
msg = "Cannot subtract offset-naive and offset-aware datetimes."
389405
with pytest.raises(TypeError, match=msg):
390406
dt_tz - ts
391407
msg = "can't subtract offset-naive and offset-aware datetimes"
392408
with pytest.raises(TypeError, match=msg):
393409
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
397410
msg = "can't subtract offset-naive and offset-aware datetimes"
398411
with pytest.raises(TypeError, match=msg):
399412
dt - dt_tz
400-
msg = "Timestamp subtraction must have the same timezones or no timezones"
413+
msg = "Cannot subtract offset-naive and offset-aware datetimes."
401414
with pytest.raises(TypeError, match=msg):
402415
ts - dt_tz
403416
with pytest.raises(TypeError, match=msg):
404417
ts_tz2 - ts
405418
with pytest.raises(TypeError, match=msg):
406419
ts_tz2 - dt
407-
with pytest.raises(TypeError, match=msg):
408-
ts_tz - ts_tz2
409420

410421
# with dti
411422
with pytest.raises(TypeError, match=msg):
412423
dti - ts_tz
413424
with pytest.raises(TypeError, match=msg):
414425
dti_tz - ts
415-
with pytest.raises(TypeError, match=msg):
416-
dti_tz - ts_tz2
426+
427+
result = dti_tz - ts_tz2
428+
expected = TimedeltaIndex(
429+
["0 days 06:00:00", "1 days 06:00:00", "2 days 06:00:00"]
430+
)
431+
tm.assert_index_equal(result, expected)
432+
433+
result = ts_tz2 - dti_tz
434+
expected = TimedeltaIndex(
435+
["-1 days +18:00:00", "-2 days +18:00:00", "-3 days +18:00:00"]
436+
)
437+
tm.assert_index_equal(result, expected)
417438

418439
result = dti_tz - dt_tz
419440
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)