Skip to content

Commit d5c0b20

Browse files
committed
ENH: Add dateutil timezone support (GH4688)
1 parent 4db583d commit d5c0b20

File tree

3 files changed

+287
-33
lines changed

3 files changed

+287
-33
lines changed

doc/source/timeseries.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,8 @@ Time Zone Handling
10481048
------------------
10491049

10501050
Using ``pytz``, pandas provides rich support for working with timestamps in
1051-
different time zones. By default, pandas objects are time zone unaware:
1051+
different time zones (pandas can also use timezones from the ``dateutil`` library).
1052+
By default, pandas objects are time zone unaware:
10521053

10531054
.. ipython:: python
10541055

pandas/tseries/tests/test_timezones.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
import os
55
import unittest
6+
import itertools
67
import nose
78

89
import numpy as np
@@ -12,6 +13,7 @@
1213
date_range, Timestamp)
1314

1415
from pandas import DatetimeIndex, Int64Index, to_datetime
16+
from pandas import tslib
1517

1618
from pandas.core.daterange import DateRange
1719
import pandas.core.datetools as datetools
@@ -39,11 +41,22 @@ def _skip_if_no_pytz():
3941
except ImportError:
4042
raise nose.SkipTest("pytz not installed")
4143

44+
def _skip_if_no_dateutil():
45+
try:
46+
import dateutil
47+
except ImportError:
48+
raise nose.SkipTest
49+
4250
try:
4351
import pytz
4452
except ImportError:
4553
pass
4654

55+
try:
56+
import dateutil
57+
except ImportError:
58+
pass
59+
4760

4861
class FixedOffset(tzinfo):
4962
"""Fixed offset in minutes east from UTC."""
@@ -958,6 +971,201 @@ def test_tzaware_offset(self):
958971
offset = dates + offsets.Hour(5)
959972
self.assertEqual(dates[0] + offsets.Hour(5), offset[0])
960973

974+
class TestPytzDateutilTimeZones(unittest.TestCase):
975+
_multiprocess_can_split_ = True
976+
FINANCIAL_TIMEZONE_NAMES = (
977+
'Africa/Johannesburg',
978+
'America/New_York', 'America/Chicago', 'America/Los_Angeles',
979+
'Asia/Bangkok', 'Asia/Hong_Kong', 'Asia/Shanghai', 'Asia/Tokyo',
980+
'Australia/Sydney',
981+
'Europe/Berlin', 'Europe/London', 'Europe/Zurich',
982+
'GMT', 'UTC',
983+
)
984+
985+
def setUp(self):
986+
_skip_if_no_pytz()
987+
_skip_if_no_dateutil()
988+
989+
def _gen_financial_timezone_pairs(self):
990+
for pair in itertools.permutations(self.FINANCIAL_TIMEZONE_NAMES, 2):
991+
yield pair
992+
993+
def _assert_two_values_same_attributes(self, a, b, attrs):
994+
for attr in attrs:
995+
tm.assert_attr_equal(attr, a, b)
996+
997+
def _assert_two_timestamp_values_same(self, a, b):
998+
self._assert_two_values_same_attributes(a, b, \
999+
('year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 'nanosecond'))
1000+
1001+
def _assert_two_datetime_values_same(self, a, b):
1002+
self._assert_two_values_same_attributes(a, b, \
1003+
('year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'))
1004+
1005+
def _clear_tslib_cache(self):
1006+
tslib.trans_cache = {}
1007+
tslib.utc_offset_cache = {}
1008+
1009+
def test_timestamp_tz_as_str(self):
1010+
"""TestPytzDateutilTimeZones: Single date with default time zone, pytz and dateutil."""
1011+
ts = Timestamp('3/11/2012 04:00', tz='US/Eastern')
1012+
exp_pytz = Timestamp('3/11/2012 04:00', tz=pytz.timezone('US/Eastern'))
1013+
exp_du = Timestamp('3/11/2012 04:00', tz=dateutil.tz.gettz('US/Eastern'))
1014+
self.assertEquals(ts, exp_pytz)
1015+
self._assert_two_timestamp_values_same(ts, exp_pytz)
1016+
self.assertEquals(ts, exp_du)
1017+
self._assert_two_timestamp_values_same(ts, exp_du)
1018+
1019+
def test_timestamp_tz_conversion(self):
1020+
"""TestPytzDateutilTimeZones: Single date time zone conversion with pytz and dateutil."""
1021+
ts_base = Timestamp('3/11/2012 04:00', tz='US/Eastern')
1022+
ts_pytz = ts_base.astimezone(pytz.timezone('Europe/Moscow'))
1023+
ts_du = ts_base.astimezone(dateutil.tz.gettz('Europe/Moscow'))
1024+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1025+
1026+
def test_eastern_london_large_year_range_jan_june(self):
1027+
"""TestPytzDateutilTimeZones: Matches Eastern->London->Eastern Jan and Jun 1st for 1970-2049."""
1028+
for yr, mo in itertools.product(range(1970, 2050), (1, 6)):
1029+
# US->Europe
1030+
ts_base = Timestamp(datetime(yr, mo, 1, 12, 0), tz=pytz.timezone('US/Eastern'))
1031+
ts_pytz = ts_base.astimezone(pytz.timezone('Europe/London'))
1032+
ts_du = ts_base.astimezone(dateutil.tz.gettz('Europe/London'))
1033+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1034+
# Europe->US
1035+
ts_base = Timestamp(datetime(yr, mo, 1, 12, 0), tz=pytz.timezone('Europe/London'))
1036+
ts_pytz = ts_base.astimezone(pytz.timezone('US/Eastern'))
1037+
ts_du = ts_base.astimezone(dateutil.tz.gettz('US/Eastern'))
1038+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1039+
1040+
def test_eastern_london_every_day_2012_2013(self):
1041+
"""TestPytzDateutilTimeZones: Matches for Eastern->London->Eastern daily for two years (one a leap year)."""
1042+
# 2012 is a leap year
1043+
for yr, mo, dy in itertools.product((2012, 2013), range(1, 13), range(1, 32)):
1044+
# US->Europe
1045+
try:
1046+
ts_base = Timestamp(datetime(yr, mo, dy, 12, 0), tz=pytz.timezone('US/Eastern'))
1047+
except ValueError:
1048+
continue
1049+
ts_pytz = ts_base.astimezone(pytz.timezone('Europe/London'))
1050+
ts_du = ts_base.astimezone(dateutil.tz.gettz('Europe/London'))
1051+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1052+
# Europe->US
1053+
ts_base = Timestamp(datetime(yr, mo, dy, 12, 0), tz=pytz.timezone('Europe/London'))
1054+
ts_pytz = ts_base.astimezone(pytz.timezone('US/Eastern'))
1055+
ts_du = ts_base.astimezone(dateutil.tz.gettz('US/Eastern'))
1056+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1057+
1058+
def test_common_financial_timezones(self):
1059+
"""TestPytzDateutilTimeZones: Permutations of time zones for major financial centres, midday, first day of each month, 2013."""
1060+
self._clear_tslib_cache()
1061+
for mo in range(1, 12):
1062+
for tz_from, tz_to in self._gen_financial_timezone_pairs():
1063+
ts_base = Timestamp(datetime(2013, mo, 1, 12, 0), tz=tz_from)
1064+
ts_pytz = ts_base.astimezone(pytz.timezone(tz_to))
1065+
ts_du = ts_base.astimezone(dateutil.tz.gettz(tz_to))
1066+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1067+
1068+
def test_common_financial_timezones_dateutil_loaded_first(self):
1069+
"""TestPytzDateutilTimeZones: Permutations of time zones for major financial centres, midday, first day of each month, 2013. dateutil timezones loaded first"""
1070+
self._clear_tslib_cache()
1071+
for mo in range(1, 12):
1072+
for tz_from, tz_to in self._gen_financial_timezone_pairs():
1073+
ts_base = Timestamp(datetime(2013, mo, 1, 12, 0), tz=tz_from)
1074+
ts_du = ts_base.astimezone(dateutil.tz.gettz(tz_to))
1075+
ts_pytz = ts_base.astimezone(pytz.timezone(tz_to))
1076+
self._assert_two_timestamp_values_same(ts_pytz, ts_du)
1077+
1078+
def test_conflict_dst_start_US_Eastern(self):
1079+
"""TestPytzDateutilTimeZones: Demonstrate that libraries disagree about start of DST, US/Eastern 2012."""
1080+
# tstamp 2012-03-11 02:00:00
1081+
# pytz: 2012-03-11 02:00:00-05:00 UTC offset -1 day, 19:00:00 UTC time: 07:00:00
1082+
#dateutil: 2012-03-11 02:00:00-04:00 UTC offset -1 day, 20:00:00 UTC time: 06:00:00
1083+
tstamp = datetime(2012, 3, 11, 2, 0)
1084+
tz_name = 'US/Eastern'
1085+
ts_pytz = pytz.timezone(tz_name).localize(tstamp)
1086+
ts_du = tstamp.replace(tzinfo=dateutil.tz.gettz(tz_name))
1087+
self.assertEqual(str(ts_pytz), '2012-03-11 02:00:00-05:00')
1088+
self.assertEqual(str(ts_du), '2012-03-11 02:00:00-04:00')
1089+
self._assert_two_datetime_values_same(ts_pytz, ts_du)
1090+
self.assertNotEqual(ts_pytz.utcoffset(), ts_du.utcoffset())
1091+
self.assertNotEqual(
1092+
str(ts_pytz.astimezone(pytz.timezone('UTC'))),
1093+
str(ts_du.astimezone(dateutil.tz.tzutc())),
1094+
)
1095+
1096+
def test_conflict_dst_start_UK(self):
1097+
"""TestPytzDateutilTimeZones: Demonstrate that libraries disagree about start of DST, Europe/London 2013."""
1098+
# tstamp 2013-03-31 01:00:00
1099+
# pytz: 2013-03-31 01:00:00+00:00 UTC offset 0:00:00 UTC time: 01:00:00
1100+
#dateutil: 2013-03-31 01:00:00+01:00 UTC offset 1:00:00 UTC time: 00:00:00
1101+
tstamp = datetime(2013, 3, 31, 1, 0)
1102+
tz_name = 'Europe/London'
1103+
ts_pytz = pytz.timezone(tz_name).localize(tstamp)
1104+
ts_du = tstamp.replace(tzinfo=dateutil.tz.gettz(tz_name))
1105+
self.assertEqual(str(ts_pytz), '2013-03-31 01:00:00+00:00')
1106+
self.assertEqual(str(ts_du), '2013-03-31 01:00:00+01:00')
1107+
self._assert_two_datetime_values_same(ts_pytz, ts_du)
1108+
self.assertNotEqual(ts_pytz.utcoffset(), ts_du.utcoffset())
1109+
self.assertNotEqual(
1110+
str(ts_pytz.astimezone(pytz.timezone('UTC'))),
1111+
str(ts_du.astimezone(dateutil.tz.tzutc())),
1112+
)
1113+
1114+
def test_date_range_us_pacific_weekly(self):
1115+
"""TestPytzDateutilTimeZones: Test a date_range weekly US/Pacific through 2012."""
1116+
range_pytz = date_range('2012-01-01 12:00', periods=52, freq='W', tz=pytz.timezone('US/Pacific'))
1117+
range_du = date_range('2012-01-01 12:00', periods=52, freq='W', tz=dateutil.tz.gettz('US/Pacific'))
1118+
for a, b in zip(range_pytz, range_du):
1119+
self.assertEquals(a, b)
1120+
1121+
def test_series_us_eastern(self):
1122+
"""TestPytzDateutilTimeZones: Test a Series with a timestamp index, US/Eastern Time across start DST 2012."""
1123+
rng = date_range('3/9/2012 12:00', periods=5, freq='D')
1124+
ts = Series(np.random.randn(len(rng)), rng)
1125+
# Localize to UTC and convert to Eastern time with default timezone library
1126+
ts_utc = ts.tz_localize('UTC')
1127+
ser_std = ts_utc.tz_convert('US/Eastern')
1128+
# Convert to Eastern time specifically with pytz
1129+
ser_pytz = ts_utc.tz_convert(pytz.timezone('US/Eastern'))
1130+
# Now with dateutil
1131+
ser_du = ts_utc.tz_convert(dateutil.tz.gettz('US/Eastern'))
1132+
# Check the indicies, firstly Timestamps
1133+
for s, p, d in zip(ser_std.index, ser_pytz.index, ser_du.index):
1134+
self.assertEquals(s, p)
1135+
self.assertEquals(s, d)
1136+
self.assertEquals(p, d)
1137+
# assert_series_equal(ser_pytz, ser_du) fails as ser_pytz.tz != ser_du.tz
1138+
self.assertTrue(np.array_equal(ser_du.index.asi8, ser_pytz.index.asi8))
1139+
self.assertNotEqual(ser_pytz.index.tz, ser_du.index.tz)
1140+
1141+
def test_series_subtract_pytz_dateutil(self):
1142+
"""TestPytzDateutilTimeZones: Create two series of Timestamps 15:00 US/Pacific from pytz and 12:00 US/Eastern from dateutil and subtract them."""
1143+
dr_pytz = date_range('2012-06-15 12:00', periods=5, freq='D').tz_localize(pytz.timezone('US/Pacific'))
1144+
dr_du = date_range('2012-06-15 15:00', periods=5, freq='D').tz_localize(dateutil.tz.gettz('US/Eastern'))
1145+
ts_pytz = Series(dr_pytz, range(5))
1146+
ts_du = Series(dr_du, range(5))
1147+
diff = ts_pytz - ts_du
1148+
# Should be 0 hours apart
1149+
exp = Series(np.zeros((5,), dtype='m8[ns]'), range(5))
1150+
self.assertEquals(diff.dtype, np.dtype('m8[ns]'))
1151+
tm.assert_series_equal(diff, exp)
1152+
# Check reverse
1153+
diff = ts_du - ts_pytz
1154+
self.assertEquals(diff.dtype, np.dtype('m8[ns]'))
1155+
tm.assert_series_equal(diff, exp)
1156+
1157+
def test_common_financial_timezones_timedelta_zero(self):
1158+
"""TestPytzDateutilTimeZones: Time zones for major financial centres in pytz and dateutil subtract to zero."""
1159+
self._clear_tslib_cache()
1160+
for mo in range(1, 12):
1161+
for tz_from, tz_to in self._gen_financial_timezone_pairs():
1162+
ts_base = Timestamp(datetime(2013, mo, 1, 12, 0), tz=tz_from)
1163+
ts_pytz = ts_base.astimezone(pytz.timezone(tz_to))
1164+
ts_du = ts_base.astimezone(dateutil.tz.gettz(tz_to))
1165+
diff = ts_pytz - ts_du
1166+
self.assertTrue(isinstance(diff, timedelta))
1167+
self.assertEqual(diff, timedelta(0), 'From: %s to: %s' % (tz_from, tz_to))
1168+
9611169
if __name__ == '__main__':
9621170
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],
9631171
exit=False)

0 commit comments

Comments
 (0)