Skip to content

Commit 6d01c3c

Browse files
committed
ENH: split out time zone localize logic from conversion, add tz_localize methods to Series, DataFrame. adjust unit tests. close #1403
1 parent a6fd608 commit 6d01c3c

File tree

7 files changed

+150
-162
lines changed

7 files changed

+150
-162
lines changed

pandas/core/generic.py

+37
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,43 @@ def tz_convert(self, tz, axis=0, copy=True):
885885
new_obj._set_axis(1, new_ax)
886886
elif axis == 1:
887887
new_obj._set_axis(0, new_ax)
888+
self._clear_item_cache()
889+
890+
return new_obj
891+
892+
def tz_localize(self, tz, axis=0, copy=True):
893+
"""
894+
Localize tz-naive TimeSeries to target time zone
895+
896+
Parameters
897+
----------
898+
tz : string or pytz.timezone object
899+
copy : boolean, default True
900+
Also make a copy of the underlying data
901+
902+
Returns
903+
-------
904+
"""
905+
axis = self._get_axis_number(axis)
906+
ax = self._get_axis(axis)
907+
908+
if not hasattr(ax, 'tz_localize'):
909+
ax_name = self._get_axis_name(axis)
910+
raise TypeError('%s is not a valid DatetimeIndex or PeriodIndex' %
911+
ax_name)
912+
913+
new_data = self._data
914+
if copy:
915+
new_data = new_data.copy()
916+
917+
new_obj = self._constructor(new_data)
918+
new_ax = ax.tz_localize(tz)
919+
920+
if axis == 0:
921+
new_obj._set_axis(1, new_ax)
922+
elif axis == 1:
923+
new_obj._set_axis(0, new_ax)
924+
self._clear_item_cache()
888925

889926
return new_obj
890927

pandas/core/series.py

+24-2
Original file line numberDiff line numberDiff line change
@@ -2830,8 +2830,7 @@ def between_time(self, start_time, end_time, include_start=True,
28302830

28312831
def tz_convert(self, tz, copy=True):
28322832
"""
2833-
Convert TimeSeries to target time zone. If it is time zone naive, it
2834-
will be localized to the passed time zone.
2833+
Convert TimeSeries to target time zone
28352834
28362835
Parameters
28372836
----------
@@ -2841,6 +2840,7 @@ def tz_convert(self, tz, copy=True):
28412840
28422841
Returns
28432842
-------
2843+
converted : TimeSeries
28442844
"""
28452845
new_index = self.index.tz_convert(tz)
28462846

@@ -2850,6 +2850,28 @@ def tz_convert(self, tz, copy=True):
28502850

28512851
return Series(new_values, index=new_index, name=self.name)
28522852

2853+
def tz_localize(self, tz, copy=True):
2854+
"""
2855+
Localize tz-naive TimeSeries to target time zone
2856+
2857+
Parameters
2858+
----------
2859+
tz : string or pytz.timezone object
2860+
copy : boolean, default True
2861+
Also make a copy of the underlying data
2862+
2863+
Returns
2864+
-------
2865+
localized : TimeSeries
2866+
"""
2867+
new_index = self.index.tz_localize(tz)
2868+
2869+
new_values = self.values
2870+
if copy:
2871+
new_values = new_values.copy()
2872+
2873+
return Series(new_values, index=new_index, name=self.name)
2874+
28532875
def to_timestamp(self, freq=None, how='start', copy=True):
28542876
"""
28552877
Cast to datetimeindex of timestamps, at *beginning* of period

pandas/src/datetime.pyx

+6-2
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ class Timestamp(_Timestamp):
181181
# tz naive, localize
182182
return Timestamp(self.to_pydatetime(), tz=tz)
183183
else:
184-
raise Exception('Cannot localize tz-aware Timestamp')
184+
raise Exception('Cannot localize tz-aware Timestamp, use '
185+
'tz_convert for conversions')
185186

186187
def tz_convert(self, tz):
187188
"""
@@ -197,7 +198,10 @@ class Timestamp(_Timestamp):
197198
converted : Timestamp
198199
"""
199200
if self.tzinfo is None:
200-
# tz naive, localize
201+
# tz naive, use tz_localize
202+
raise Exception('Cannot convert tz-naive Timestamp, use '
203+
'tz_localize to localize')
204+
201205
return Timestamp(self.to_pydatetime(), tz=tz)
202206
else:
203207
# Same UTC timestamp, different time zone

pandas/src/engines.pyx

+8-5
Original file line numberDiff line numberDiff line change
@@ -454,11 +454,14 @@ cpdef convert_scalar(ndarray arr, object value):
454454

455455
cdef inline _to_i8(object val):
456456
cdef pandas_datetimestruct dts
457-
if util.is_datetime64_object(val):
458-
val = get_datetime64_value(val)
459-
elif PyDateTime_Check(val):
460-
return _pydatetime_to_dts(val, &dts)
461-
return val
457+
try:
458+
return val.value
459+
except AttributeError:
460+
if util.is_datetime64_object(val):
461+
return get_datetime64_value(val)
462+
elif PyDateTime_Check(val):
463+
return _pydatetime_to_dts(val, &dts)
464+
return val
462465

463466

464467
# ctypedef fused idxvalue_t:

pandas/tseries/index.py

+6-118
Original file line numberDiff line numberDiff line change
@@ -322,10 +322,6 @@ def _generate(cls, start, end, periods, name, offset,
322322
# Convert local to UTC
323323
ints = index.view('i8')
324324
index = lib.tz_localize_to_utc(ints, tz)
325-
326-
# lib.tz_localize_check(ints, tz)
327-
# index = lib.tz_convert(ints, tz, _utc())
328-
329325
index = index.view(_NS_DTYPE)
330326

331327
index = index.view(cls)
@@ -1174,7 +1170,9 @@ def tz_convert(self, tz):
11741170
tz = tools._maybe_get_tz(tz)
11751171

11761172
if self.tz is None:
1177-
return self.tz_localize(tz)
1173+
# tz naive, use tz_localize
1174+
raise Exception('Cannot convert tz-naive timestamps, use '
1175+
'tz_localize to localize')
11781176

11791177
# No conversion since timestamps are all UTC to begin with
11801178
return self._simple_new(self.values, self.name, self.offset, tz)
@@ -1188,15 +1186,13 @@ def tz_localize(self, tz):
11881186
localized : DatetimeIndex
11891187
"""
11901188
if self.tz is not None:
1191-
raise ValueError("Already have timezone info, "
1192-
"use tz_convert to convert.")
1189+
raise ValueError("Already tz-aware, use tz_convert to convert.")
11931190
tz = tools._maybe_get_tz(tz)
11941191

1195-
lib.tz_localize_check(self.asi8, tz)
1196-
11971192
# Convert to UTC
1198-
new_dates = lib.tz_convert(self.asi8, tz, _utc())
1193+
new_dates = lib.tz_localize_to_utc(self.asi8, tz)
11991194
new_dates = new_dates.view(_NS_DTYPE)
1195+
12001196
return self._simple_new(new_dates, self.name, self.offset, tz)
12011197

12021198
def tz_validate(self):
@@ -1355,111 +1351,3 @@ def _time_to_nanosecond(time):
13551351
return (1000000 * seconds + time.microsecond) * 1000
13561352

13571353

1358-
1359-
def tz_localize_to_utc(vals, tz):
1360-
"""
1361-
Localize tzinfo-naive DateRange to given time zone (using pytz). If
1362-
there are ambiguities in the values, raise AmbiguousTimeError.
1363-
1364-
Returns
1365-
-------
1366-
localized : DatetimeIndex
1367-
"""
1368-
# Vectorized version of DstTzInfo.localize
1369-
1370-
# if not have_pytz:
1371-
# raise Exception("Could not find pytz module")
1372-
import pytz
1373-
1374-
n = len(vals)
1375-
DAY_NS = 86400000000000L
1376-
NPY_NAT = lib.iNaT
1377-
1378-
if tz == pytz.utc or tz is None:
1379-
return vals
1380-
1381-
trans = _get_transitions(tz) # transition dates
1382-
deltas = _get_deltas(tz) # utc offsets
1383-
1384-
result = np.empty(n, dtype=np.int64)
1385-
result_a = np.empty(n, dtype=np.int64)
1386-
result_b = np.empty(n, dtype=np.int64)
1387-
result_a.fill(NPY_NAT)
1388-
result_b.fill(NPY_NAT)
1389-
1390-
# left side
1391-
idx_shifted = np.maximum(0, trans.searchsorted(vals - DAY_NS,
1392-
side='right') - 1)
1393-
1394-
for i in range(n):
1395-
v = vals[i] - deltas[idx_shifted[i]]
1396-
pos = trans.searchsorted(v, side='right') - 1
1397-
1398-
# timestamp falls to the left side of the DST transition
1399-
if v + deltas[pos] == vals[i]:
1400-
result_a[i] = v
1401-
1402-
# right side
1403-
idx_shifted = np.maximum(0, trans.searchsorted(vals + DAY_NS,
1404-
side='right') - 1)
1405-
1406-
for i in range(n):
1407-
v = vals[i] - deltas[idx_shifted[i]]
1408-
pos = trans.searchsorted(v, side='right') - 1
1409-
1410-
# timestamp falls to the right side of the DST transition
1411-
if v + deltas[pos] == vals[i]:
1412-
result_b[i] = v
1413-
1414-
for i in range(n):
1415-
left = result_a[i]
1416-
right = result_b[i]
1417-
if left != NPY_NAT and right != NPY_NAT:
1418-
if left == right:
1419-
result[i] = left
1420-
else:
1421-
stamp = Timestamp(vals[i])
1422-
raise pytz.AmbiguousTimeError(stamp)
1423-
elif left != NPY_NAT:
1424-
result[i] = left
1425-
elif right != NPY_NAT:
1426-
result[i] = right
1427-
else:
1428-
stamp = Timestamp(vals[i])
1429-
raise pytz.NonExistentTimeError(stamp)
1430-
1431-
return result
1432-
1433-
1434-
trans_cache = {}
1435-
utc_offset_cache = {}
1436-
1437-
def _get_transitions(tz):
1438-
"""
1439-
Get UTC times of DST transitions
1440-
"""
1441-
if tz not in trans_cache:
1442-
arr = np.array(tz._utc_transition_times, dtype='M8[ns]')
1443-
trans_cache[tz] = arr.view('i8')
1444-
return trans_cache[tz]
1445-
1446-
def _get_deltas(tz):
1447-
"""
1448-
Get UTC offsets in microseconds corresponding to DST transitions
1449-
"""
1450-
if tz not in utc_offset_cache:
1451-
utc_offset_cache[tz] = _unbox_utcoffsets(tz._transition_info)
1452-
return utc_offset_cache[tz]
1453-
1454-
def total_seconds(td): # Python 2.6 compat
1455-
return ((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) //
1456-
10**6)
1457-
1458-
def _unbox_utcoffsets(transinfo):
1459-
sz = len(transinfo)
1460-
arr = np.empty(sz, dtype='i8')
1461-
1462-
for i in range(sz):
1463-
arr[i] = int(total_seconds(transinfo[i][0])) * 1000000000
1464-
1465-
return arr

pandas/tseries/tests/test_timeseries.py

+11-21
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ def assert_range_equal(left, right):
9292
assert(left.tz == right.tz)
9393

9494

95+
def _skip_if_no_pytz():
96+
try:
97+
import pytz
98+
except ImportError:
99+
raise nose.SkipTest
100+
101+
95102
class TestTimeSeries(unittest.TestCase):
96103

97104
def test_dti_slicing(self):
@@ -140,6 +147,7 @@ def test_series_box_timestamp(self):
140147
self.assert_(isinstance(s[5], Timestamp))
141148

142149
def test_timestamp_to_datetime(self):
150+
_skip_if_no_pytz()
143151
rng = date_range('20090415', '20090519',
144152
tz='US/Eastern')
145153

@@ -149,6 +157,8 @@ def test_timestamp_to_datetime(self):
149157
self.assertEquals(stamp.tzinfo, dtval.tzinfo)
150158

151159
def test_index_convert_to_datetime_array(self):
160+
_skip_if_no_pytz()
161+
152162
def _check_rng(rng):
153163
converted = rng.to_pydatetime()
154164
self.assert_(isinstance(converted, np.ndarray))
@@ -1495,6 +1505,7 @@ def test_comparison(self):
14951505
self.assert_(other >= val)
14961506

14971507
def test_cant_compare_tz_naive_w_aware(self):
1508+
_skip_if_no_pytz()
14981509
# #1404
14991510
a = Timestamp('3/12/2012')
15001511
b = Timestamp('3/12/2012', tz='utc')
@@ -1516,27 +1527,6 @@ def test_delta_preserve_nanos(self):
15161527
result = val + timedelta(1)
15171528
self.assert_(result.nanosecond == val.nanosecond)
15181529

1519-
def test_tz_convert_localize(self):
1520-
stamp = Timestamp('3/11/2012 04:00')
1521-
1522-
result = stamp.tz_convert('US/Eastern')
1523-
expected = Timestamp('3/11/2012 04:00', tz='US/Eastern')
1524-
self.assertEquals(result.hour, expected.hour)
1525-
self.assertEquals(result, expected)
1526-
1527-
def test_timedelta_push_over_dst_boundary(self):
1528-
# #1389
1529-
1530-
# 4 hours before DST transition
1531-
stamp = Timestamp('3/10/2012 22:00', tz='US/Eastern')
1532-
1533-
result = stamp + timedelta(hours=6)
1534-
1535-
# spring forward, + "7" hours
1536-
expected = Timestamp('3/11/2012 05:00', tz='US/Eastern')
1537-
1538-
self.assertEquals(result, expected)
1539-
15401530
def test_frequency_misc(self):
15411531
self.assertEquals(fmod.get_freq_group('T'),
15421532
fmod.FreqGroup.FR_MIN)

0 commit comments

Comments
 (0)