Skip to content

Commit f35209e

Browse files
mroeschkejreback
authored andcommitted
BUG: Correct Timestamp localization with tz near DST (#11481) (#15934)
* BUG: Timestamp doesn't respect tz DST closes #11481 closes #15777 * DOC: add doc-strings to tz_convert/tz_localize in tslib.pyx TST: more tests, xref #15823, xref #11708
1 parent 4e38396 commit f35209e

File tree

4 files changed

+91
-7
lines changed

4 files changed

+91
-7
lines changed

doc/source/whatsnew/v0.20.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,7 @@ Conversion
11241124
- Bug in ``Timestamp.replace`` now raises ``TypeError`` when incorrect argument names are given; previously this raised ``ValueError`` (:issue:`15240`)
11251125
- Bug in ``Timestamp.replace`` with compat for passing long integers (:issue:`15030`)
11261126
- Bug in ``Timestamp`` returning UTC based time/date attributes when a timezone was provided (:issue:`13303`)
1127+
- Bug in ``Timestamp`` incorrectly localizing timezones during construction (:issue:`11481`, :issue:`15777`)
11271128
- Bug in ``TimedeltaIndex`` addition where overflow was being allowed without error (:issue:`14816`)
11281129
- Bug in ``TimedeltaIndex`` raising a ``ValueError`` when boolean indexing with ``loc`` (:issue:`14946`)
11291130
- Bug in catching an overflow in ``Timestamp`` + ``Timedelta/Offset`` operations (:issue:`15126`)

pandas/_libs/tslib.pyx

+38-2
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,9 @@ cpdef convert_str_to_tsobject(object ts, object tz, object unit,
15691569
ts = obj.value
15701570
if tz is not None:
15711571
# shift for _localize_tso
1572-
ts = tz_convert_single(ts, tz, 'UTC')
1572+
ts = tz_localize_to_utc(np.array([ts], dtype='i8'), tz,
1573+
ambiguous='raise',
1574+
errors='raise')[0]
15731575
except ValueError:
15741576
try:
15751577
ts = parse_datetime_string(
@@ -4073,7 +4075,23 @@ except:
40734075
have_pytz = False
40744076

40754077

4078+
@cython.boundscheck(False)
4079+
@cython.wraparound(False)
40764080
def tz_convert(ndarray[int64_t] vals, object tz1, object tz2):
4081+
"""
4082+
Convert the values (in i8) from timezone1 to timezone2
4083+
4084+
Parameters
4085+
----------
4086+
vals : int64 ndarray
4087+
tz1 : string / timezone object
4088+
tz2 : string / timezone object
4089+
4090+
Returns
4091+
-------
4092+
int64 ndarray of converted
4093+
"""
4094+
40774095
cdef:
40784096
ndarray[int64_t] utc_dates, tt, result, trans, deltas
40794097
Py_ssize_t i, j, pos, n = len(vals)
@@ -4175,6 +4193,23 @@ def tz_convert(ndarray[int64_t] vals, object tz1, object tz2):
41754193

41764194

41774195
def tz_convert_single(int64_t val, object tz1, object tz2):
4196+
"""
4197+
Convert the val (in i8) from timezone1 to timezone2
4198+
4199+
This is a single timezone versoin of tz_convert
4200+
4201+
Parameters
4202+
----------
4203+
val : int64
4204+
tz1 : string / timezone object
4205+
tz2 : string / timezone object
4206+
4207+
Returns
4208+
-------
4209+
int64 converted
4210+
4211+
"""
4212+
41784213
cdef:
41794214
ndarray[int64_t] trans, deltas
41804215
Py_ssize_t pos
@@ -4374,7 +4409,7 @@ cpdef ndarray _unbox_utcoffsets(object transinfo):
43744409
def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None,
43754410
object errors='raise'):
43764411
"""
4377-
Localize tzinfo-naive DateRange to given time zone (using pytz). If
4412+
Localize tzinfo-naive i8 to given time zone (using pytz). If
43784413
there are ambiguities in the values, raise AmbiguousTimeError.
43794414
43804415
Returns
@@ -4546,6 +4581,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None,
45464581

45474582
return result
45484583

4584+
45494585
cdef inline bisect_right_i8(int64_t *data, int64_t val, Py_ssize_t n):
45504586
cdef Py_ssize_t pivot, left = 0, right = n
45514587

pandas/tests/series/test_indexing.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1024,9 +1024,9 @@ def test_setitem_with_tz_dst(self):
10241024
# scalar
10251025
s = orig.copy()
10261026
s[1] = pd.Timestamp('2011-01-01', tz=tz)
1027-
exp = pd.Series([pd.Timestamp('2016-11-06 00:00', tz=tz),
1028-
pd.Timestamp('2011-01-01 00:00', tz=tz),
1029-
pd.Timestamp('2016-11-06 02:00', tz=tz)])
1027+
exp = pd.Series([pd.Timestamp('2016-11-06 00:00-04:00', tz=tz),
1028+
pd.Timestamp('2011-01-01 00:00-05:00', tz=tz),
1029+
pd.Timestamp('2016-11-06 01:00-05:00', tz=tz)])
10301030
tm.assert_series_equal(s, exp)
10311031

10321032
s = orig.copy()

pandas/tests/tseries/test_timezones.py

+49-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# pylint: disable-msg=E1101,W0612
2+
import pytest
23
import pytz
34
import numpy as np
45
from distutils.version import LooseVersion
@@ -159,6 +160,52 @@ def test_timestamp_constructed_by_date_and_tz_explicit(self):
159160
self.assertEqual(result.hour, expected.hour)
160161
self.assertEqual(result, expected)
161162

163+
def test_timestamp_constructor_near_dst_boundary(self):
164+
# GH 11481 & 15777
165+
# Naive string timestamps were being localized incorrectly
166+
# with tz_convert_single instead of tz_localize_to_utc
167+
168+
for tz in ['Europe/Brussels', 'Europe/Prague']:
169+
result = Timestamp('2015-10-25 01:00', tz=tz)
170+
expected = Timestamp('2015-10-25 01:00').tz_localize(tz)
171+
assert result == expected
172+
173+
with pytest.raises(pytz.AmbiguousTimeError):
174+
Timestamp('2015-10-25 02:00', tz=tz)
175+
176+
result = Timestamp('2017-03-26 01:00', tz='Europe/Paris')
177+
expected = Timestamp('2017-03-26 01:00').tz_localize('Europe/Paris')
178+
assert result == expected
179+
180+
with pytest.raises(pytz.NonExistentTimeError):
181+
Timestamp('2017-03-26 02:00', tz='Europe/Paris')
182+
183+
# GH 11708
184+
result = to_datetime("2015-11-18 15:30:00+05:30").tz_localize(
185+
'UTC').tz_convert('Asia/Kolkata')
186+
expected = Timestamp('2015-11-18 15:30:00+0530', tz='Asia/Kolkata')
187+
assert result == expected
188+
189+
# GH 15823
190+
result = Timestamp('2017-03-26 00:00', tz='Europe/Paris')
191+
expected = Timestamp('2017-03-26 00:00:00+0100', tz='Europe/Paris')
192+
assert result == expected
193+
194+
result = Timestamp('2017-03-26 01:00', tz='Europe/Paris')
195+
expected = Timestamp('2017-03-26 01:00:00+0100', tz='Europe/Paris')
196+
assert result == expected
197+
198+
with pytest.raises(pytz.NonExistentTimeError):
199+
Timestamp('2017-03-26 02:00', tz='Europe/Paris')
200+
result = Timestamp('2017-03-26 02:00:00+0100', tz='Europe/Paris')
201+
expected = Timestamp(result.value).tz_localize(
202+
'UTC').tz_convert('Europe/Paris')
203+
assert result == expected
204+
205+
result = Timestamp('2017-03-26 03:00', tz='Europe/Paris')
206+
expected = Timestamp('2017-03-26 03:00:00+0200', tz='Europe/Paris')
207+
assert result == expected
208+
162209
def test_timestamp_to_datetime_tzoffset(self):
163210
# tzoffset
164211
from dateutil.tz import tzoffset
@@ -517,8 +564,8 @@ def f():
517564
freq="H"))
518565
if dateutil.__version__ != LooseVersion('2.6.0'):
519566
# GH 14621
520-
self.assertEqual(times[-1], Timestamp('2013-10-27 01:00', tz=tz,
521-
freq="H"))
567+
self.assertEqual(times[-1], Timestamp('2013-10-27 01:00:00+0000',
568+
tz=tz, freq="H"))
522569

523570
def test_ambiguous_nat(self):
524571
tz = self.tz('US/Eastern')

0 commit comments

Comments
 (0)