diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 5897b1a43054f..48acacd7ced08 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -1244,21 +1244,53 @@ the quarter end: Time Zone Handling ------------------ -Using ``pytz``, pandas provides rich support for working with timestamps in -different time zones. By default, pandas objects are time zone unaware: +Pandas provides rich support for working with timestamps in different time zones using ``pytz`` and ``dateutil`` libraries. +``dateutil`` support is new [in 0.14.1] and currently only supported for fixed offset and tzfile zones. The default library is ``pytz``. +Support for ``dateutil`` is provided for compatibility with other applications e.g. if you use ``dateutil`` in other python packages. + +By default, pandas objects are time zone unaware: .. ipython:: python rng = date_range('3/6/2012 00:00', periods=15, freq='D') - print(rng.tz) + rng.tz is None To supply the time zone, you can use the ``tz`` keyword to ``date_range`` and -other functions: +other functions. Dateutil time zone strings are distinguished from ``pytz`` +time zones by starting with ``dateutil/``. In ``pytz`` you can find a list of +common (and less common) time zones using ``from pytz import common_timezones, all_timezones``. +``dateutil`` uses the OS timezones so there isn't a fixed list available. For +common zones, the names are the same as ``pytz``. .. ipython:: python - + + # pytz rng_utc = date_range('3/6/2012 00:00', periods=10, freq='D', tz='UTC') - print(rng_utc.tz) + rng_utc.tz + + # dateutil + rng_utc_dateutil = date_range('3/6/2012 00:00', periods=10, freq='D', + tz='dateutil/UTC') + rng_utc_dateutil.tz + +You can also construct the timezone explicitly first, which gives you more control over which +time zone is used: + +.. ipython:: python + + # pytz + import pytz + tz_pytz = pytz.timezone('UTC') + rng_utc = date_range('3/6/2012 00:00', periods=10, freq='D', tz=tz_pytz) + rng_utc.tz + + # dateutil + import dateutil + tz_dateutil = dateutil.tz.gettz('UTC') + rng_utc_dateutil = date_range('3/6/2012 00:00', periods=10, freq='D', + tz=tz_dateutil) + rng_utc_dateutil.tz + Timestamps, like Python's ``datetime.datetime`` object can be either time zone naive or time zone aware. Naive time series and DatetimeIndex objects can be @@ -1271,6 +1303,7 @@ naive or time zone aware. Naive time series and DatetimeIndex objects can be ts_utc = ts.tz_localize('UTC') ts_utc +Again, you can explicitly construct the timezone object first. You can use the ``tz_convert`` method to convert pandas objects to convert tz-aware data to another time zone: @@ -1278,6 +1311,11 @@ tz-aware data to another time zone: ts_utc.tz_convert('US/Eastern') +.. warning:: + Be very wary of conversions between libraries as ``pytz`` and ``dateutil`` + may have different definitions of the time zones. This is more of a problem for + unusual timezones than for 'standard' zones like ``US/Eastern``. + Under the hood, all timestamps are stored in UTC. Scalar values from a ``DatetimeIndex`` with a time zone will have their fields (day, hour, minute) localized to the time zone. However, timestamps with the same UTC value are diff --git a/doc/source/v0.14.1.txt b/doc/source/v0.14.1.txt index 48eac7fb1b761..e4effc1c78798 100644 --- a/doc/source/v0.14.1.txt +++ b/doc/source/v0.14.1.txt @@ -9,6 +9,8 @@ users upgrade to this version. - Highlights include: + - Support for ``dateutil`` timezones. + - :ref:`Other Enhancements ` - :ref:`API Changes ` @@ -53,6 +55,17 @@ Enhancements ~~~~~~~~~~~~ - Tests for basic reading of public S3 buckets now exist (:issue:`7281`). +- Support for dateutil timezones, which can now be used in the same way as + pytz timezones across pandas. (:issue:`4688`) + + .. ipython:: python + + rng_utc_dateutil = date_range('3/6/2012 00:00', periods=10, freq='D', + tz='dateutil/UTC') + rng_utc_dateutil.tz + + See :ref:`the docs `. + .. _whatsnew_0141.performance: Performance diff --git a/pandas/index.pyx b/pandas/index.pyx index 4f8e780ded808..3dcdbf207fb3f 100644 --- a/pandas/index.pyx +++ b/pandas/index.pyx @@ -605,11 +605,11 @@ cdef inline _to_i8(object val): return get_datetime64_value(val) elif PyDateTime_Check(val): tzinfo = getattr(val, 'tzinfo', None) - val = _pydatetime_to_dts(val, &dts) + ival = _pydatetime_to_dts(val, &dts) # Save the original date value so we can get the utcoffset from it. if tzinfo is not None and not _is_utc(tzinfo): offset = tslib._get_utcoffset(tzinfo, val) - val -= tslib._delta_to_nanoseconds(offset) - + ival -= tslib._delta_to_nanoseconds(offset) + return ival return val cdef inline bint _is_utc(object tz): diff --git a/pandas/io/pytables.py b/pandas/io/pytables.py index 9fabf0ae960fe..cee1867e73179 100644 --- a/pandas/io/pytables.py +++ b/pandas/io/pytables.py @@ -1729,11 +1729,23 @@ def set_atom(self, block, block_items, existing_col, min_itemsize, if getattr(rvalues[0], 'tzinfo', None) is not None: # if this block has more than one timezone, raise - if len(set([r.tzinfo for r in rvalues])) != 1: - raise TypeError( - "too many timezones in this block, create separate " - "data columns" - ) + try: + # pytz timezones: compare on zone name (to avoid issues with DST being a different zone to STD). + zones = [r.tzinfo.zone for r in rvalues] + except: + # dateutil timezones: compare on == + zones = [r.tzinfo for r in rvalues] + if any(zones[0] != zone_i for zone_i in zones[1:]): + raise TypeError( + "too many timezones in this block, create separate " + "data columns" + ) + else: + if len(set(zones)) != 1: + raise TypeError( + "too many timezones in this block, create separate " + "data columns" + ) # convert this column to datetime64[ns] utc, and save the tz index = DatetimeIndex(rvalues) diff --git a/pandas/io/tests/test_json/test_ujson.py b/pandas/io/tests/test_json/test_ujson.py index 32057f9ffd35c..fcd5515419537 100644 --- a/pandas/io/tests/test_json/test_ujson.py +++ b/pandas/io/tests/test_json/test_ujson.py @@ -25,6 +25,7 @@ assert_array_almost_equal_nulp, assert_approx_equal) import pytz +import dateutil from pandas import DataFrame, Series, Index, NaT, DatetimeIndex import pandas.util.testing as tm @@ -361,7 +362,9 @@ def test_encodeTimeConversion(self): datetime.time(), datetime.time(1, 2, 3), datetime.time(10, 12, 15, 343243), - datetime.time(10, 12, 15, 343243, pytz.utc)] + datetime.time(10, 12, 15, 343243, pytz.utc), +# datetime.time(10, 12, 15, 343243, dateutil.tz.gettz('UTC')), # this segfaults! No idea why. + ] for test in tests: output = ujson.encode(test) expected = '"%s"' % test.isoformat() diff --git a/pandas/io/tests/test_pytables.py b/pandas/io/tests/test_pytables.py index 77555ad81a45b..edaae26acb29e 100644 --- a/pandas/io/tests/test_pytables.py +++ b/pandas/io/tests/test_pytables.py @@ -1991,7 +1991,7 @@ def test_unimplemented_dtypes_table_columns(self): # this fails because we have a date in the object block...... self.assertRaises(TypeError, store.append, 'df_unimplemented', df) - def test_append_with_timezones(self): + def test_append_with_timezones_pytz(self): from datetime import timedelta @@ -2020,7 +2020,8 @@ def compare(a,b): compare(store.select('df_tz',where=Term('A>=df.A[3]')),df[df.A>=df.A[3]]) _maybe_remove(store, 'df_tz') - df = DataFrame(dict(A = Timestamp('20130102',tz='US/Eastern'), B = Timestamp('20130103',tz='US/Eastern')),index=range(5)) + # ensure we include dates in DST and STD time here. + df = DataFrame(dict(A = Timestamp('20130102',tz='US/Eastern'), B = Timestamp('20130603',tz='US/Eastern')),index=range(5)) store.append('df_tz',df) result = store['df_tz'] compare(result,df) @@ -2057,6 +2058,78 @@ def compare(a,b): result = store.select('df') assert_frame_equal(result,df) + def test_append_with_timezones_dateutil(self): + + from datetime import timedelta + + try: + import dateutil + except ImportError: + raise nose.SkipTest + + def compare(a, b): + tm.assert_frame_equal(a, b) + + # compare the zones on each element + for c in a.columns: + for i in a.index: + a_e = a[c][i] + b_e = b[c][i] + if not (a_e == b_e and a_e.tz == b_e.tz): + raise AssertionError("invalid tz comparsion [%s] [%s]" % (a_e, b_e)) + + # as columns + with ensure_clean_store(self.path) as store: + + _maybe_remove(store, 'df_tz') + df = DataFrame(dict(A=[ Timestamp('20130102 2:00:00', tz=dateutil.tz.gettz('US/Eastern')) + timedelta(hours=1) * i for i in range(5) ])) + store.append('df_tz', df, data_columns=['A']) + result = store['df_tz'] + compare(result, df) + assert_frame_equal(result, df) + + # select with tz aware + compare(store.select('df_tz', where=Term('A>=df.A[3]')), df[df.A >= df.A[3]]) + + _maybe_remove(store, 'df_tz') + # ensure we include dates in DST and STD time here. + df = DataFrame(dict(A=Timestamp('20130102', tz=dateutil.tz.gettz('US/Eastern')), B=Timestamp('20130603', tz=dateutil.tz.gettz('US/Eastern'))), index=range(5)) + store.append('df_tz', df) + result = store['df_tz'] + compare(result, df) + assert_frame_equal(result, df) + + _maybe_remove(store, 'df_tz') + df = DataFrame(dict(A=Timestamp('20130102', tz=dateutil.tz.gettz('US/Eastern')), B=Timestamp('20130102', tz=dateutil.tz.gettz('EET'))), index=range(5)) + self.assertRaises(TypeError, store.append, 'df_tz', df) + + # this is ok + _maybe_remove(store, 'df_tz') + store.append('df_tz', df, data_columns=['A', 'B']) + result = store['df_tz'] + compare(result, df) + assert_frame_equal(result, df) + + # can't append with diff timezone + df = DataFrame(dict(A=Timestamp('20130102', tz=dateutil.tz.gettz('US/Eastern')), B=Timestamp('20130102', tz=dateutil.tz.gettz('CET'))), index=range(5)) + self.assertRaises(ValueError, store.append, 'df_tz', df) + + # as index + with ensure_clean_store(self.path) as store: + + # GH 4098 example + df = DataFrame(dict(A=Series(lrange(3), index=date_range('2000-1-1', periods=3, freq='H', tz=dateutil.tz.gettz('US/Eastern'))))) + + _maybe_remove(store, 'df') + store.put('df', df) + result = store.select('df') + assert_frame_equal(result, df) + + _maybe_remove(store, 'df') + store.append('df', df) + result = store.select('df') + assert_frame_equal(result, df) + def test_store_timezone(self): # GH2852 # issue storing datetime.date with a timezone as it resets when read back in a new timezone diff --git a/pandas/tests/test_format.py b/pandas/tests/test_format.py index 8e405dc98f3da..302b8ca9983e0 100644 --- a/pandas/tests/test_format.py +++ b/pandas/tests/test_format.py @@ -92,6 +92,13 @@ def _skip_if_no_pytz(): except ImportError: raise nose.SkipTest("pytz not installed") +def _skip_if_no_dateutil(): + try: + import dateutil + except ImportError: + raise nose.SkipTest("dateutil not installed") + + class TestDataFrameFormatting(tm.TestCase): _multiprocess_can_split_ = True @@ -2922,7 +2929,7 @@ def test_no_tz(self): ts_nanos_micros = Timestamp(1200) self.assertEqual(str(ts_nanos_micros), "1970-01-01 00:00:00.000001200") - def test_tz(self): + def test_tz_pytz(self): _skip_if_no_pytz() import pytz @@ -2936,6 +2943,20 @@ def test_tz(self): dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=pytz.utc) self.assertEqual(str(dt_datetime_us), str(Timestamp(dt_datetime_us))) + def test_tz_dateutil(self): + _skip_if_no_dateutil() + import dateutil + utc = dateutil.tz.gettz('UTC') + + dt_date = datetime(2013, 1, 2, tzinfo=utc) + self.assertEqual(str(dt_date), str(Timestamp(dt_date))) + + dt_datetime = datetime(2013, 1, 2, 12, 1, 3, tzinfo=utc) + self.assertEqual(str(dt_datetime), str(Timestamp(dt_datetime))) + + dt_datetime_us = datetime(2013, 1, 2, 12, 1, 3, 45, tzinfo=utc) + self.assertEqual(str(dt_datetime_us), str(Timestamp(dt_datetime_us))) + if __name__ == '__main__': import nose nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index bc12cc5aaaa3b..44587248e6d51 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -47,6 +47,12 @@ def _skip_if_no_pytz(): except ImportError: raise nose.SkipTest("pytz not installed") +def _skip_if_no_dateutil(): + try: + import dateutil + except ImportError: + raise nose.SkipTest("dateutil not installed") + #------------------------------------------------------------------------------ # Series test cases @@ -4573,7 +4579,7 @@ def test_getitem_setitem_datetimeindex(self): result["1990-01-02"] = ts[24:48] assert_series_equal(result, ts) - def test_getitem_setitem_datetime_tz(self): + def test_getitem_setitem_datetime_tz_pytz(self): _skip_if_no_pytz(); from pytz import timezone as tz @@ -4608,6 +4614,39 @@ def test_getitem_setitem_datetime_tz(self): result[date] = ts[4] assert_series_equal(result, ts) + + def test_getitem_setitem_datetime_tz_dateutil(self): + _skip_if_no_dateutil(); + from dateutil.tz import gettz as tz + + from pandas import date_range + N = 50 + # testing with timezone, GH #2785 + rng = date_range('1/1/1990', periods=N, freq='H', tz='US/Eastern') + ts = Series(np.random.randn(N), index=rng) + + # also test Timestamp tz handling, GH #2789 + result = ts.copy() + result["1990-01-01 09:00:00+00:00"] = 0 + result["1990-01-01 09:00:00+00:00"] = ts[4] + assert_series_equal(result, ts) + + result = ts.copy() + result["1990-01-01 03:00:00-06:00"] = 0 + result["1990-01-01 03:00:00-06:00"] = ts[4] + assert_series_equal(result, ts) + + # repeat with datetimes + result = ts.copy() + result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = 0 + result[datetime(1990, 1, 1, 9, tzinfo=tz('UTC'))] = ts[4] + assert_series_equal(result, ts) + + result = ts.copy() + result[datetime(1990, 1, 1, 3, tzinfo=tz('US/Central'))] = 0 + result[datetime(1990, 1, 1, 3, tzinfo=tz('US/Central'))] = ts[4] + assert_series_equal(result, ts) + def test_getitem_setitem_periodindex(self): from pandas import period_range N = 50 diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py index 3612b9dbeafb3..34b0045b4983b 100644 --- a/pandas/tseries/index.py +++ b/pandas/tseries/index.py @@ -1617,7 +1617,7 @@ def _view_like(self, ndarray): def tz_convert(self, tz): """ - Convert DatetimeIndex from one time zone to another (using pytz) + Convert DatetimeIndex from one time zone to another (using pytz/dateutil) Returns ------- @@ -1635,11 +1635,11 @@ def tz_convert(self, tz): def tz_localize(self, tz, infer_dst=False): """ - Localize tz-naive DatetimeIndex to given time zone (using pytz) + Localize tz-naive DatetimeIndex to given time zone (using pytz/dateutil) Parameters ---------- - tz : string or pytz.timezone + tz : string or pytz.timezone or dateutil.tz.tzfile Time zone for time. Corresponding timestamps would be converted to time zone of the TimeSeries infer_dst : boolean, default False @@ -1666,7 +1666,7 @@ def indexer_at_time(self, time, asof=False): Parameters ---------- time : datetime.time or string - tz : string or pytz.timezone + tz : string or pytz.timezone or dateutil.tz.tzfile Time zone for time. Corresponding timestamps would be converted to time zone of the TimeSeries @@ -1701,7 +1701,7 @@ def indexer_between_time(self, start_time, end_time, include_start=True, end_time : datetime.time or string include_start : boolean, default True include_end : boolean, default True - tz : string or pytz.timezone, default None + tz : string or pytz.timezone or dateutil.tz.tzfile, default None Returns ------- diff --git a/pandas/tseries/tests/test_daterange.py b/pandas/tseries/tests/test_daterange.py index 159e3d1603b20..dd84ee27caf0e 100644 --- a/pandas/tseries/tests/test_daterange.py +++ b/pandas/tseries/tests/test_daterange.py @@ -24,6 +24,12 @@ def _skip_if_no_pytz(): except ImportError: raise nose.SkipTest("pytz not installed") +def _skip_if_no_dateutil(): + try: + import dateutil + except ImportError: + raise nose.SkipTest("dateutil not installed") + def _skip_if_no_cday(): if datetools.cday is None: @@ -291,6 +297,11 @@ def test_summary_pytz(self): import pytz bdate_range('1/1/2005', '1/1/2009', tz=pytz.utc).summary() + def test_summary_dateutil(self): + _skip_if_no_dateutil() + import dateutil + bdate_range('1/1/2005', '1/1/2009', tz=dateutil.tz.gettz('UTC')).summary() + def test_misc(self): end = datetime(2009, 5, 13) dr = bdate_range(end=end, periods=20) @@ -354,7 +365,7 @@ def test_range_bug(self): exp_values = [start + i * offset for i in range(5)] self.assert_numpy_array_equal(result, DatetimeIndex(exp_values)) - def test_range_tz(self): + def test_range_tz_pytz(self): # GH 2906 _skip_if_no_pytz() from pytz import timezone as tz @@ -377,7 +388,30 @@ def test_range_tz(self): self.assertEqual(dr[0], start) self.assertEqual(dr[2], end) - def test_month_range_union_tz(self): + def test_range_tz_dateutil(self): + # GH 2906 + _skip_if_no_dateutil() + from dateutil.tz import gettz as tz + + start = datetime(2011, 1, 1, tzinfo=tz('US/Eastern')) + end = datetime(2011, 1, 3, tzinfo=tz('US/Eastern')) + + dr = date_range(start=start, periods=3) + self.assert_(dr.tz == tz('US/Eastern')) + self.assert_(dr[0] == start) + self.assert_(dr[2] == end) + + dr = date_range(end=end, periods=3) + self.assert_(dr.tz == tz('US/Eastern')) + self.assert_(dr[0] == start) + self.assert_(dr[2] == end) + + dr = date_range(start=start, end=end) + self.assert_(dr.tz == tz('US/Eastern')) + self.assert_(dr[0] == start) + self.assert_(dr[2] == end) + + def test_month_range_union_tz_pytz(self): _skip_if_no_pytz() from pytz import timezone tz = timezone('US/Eastern') @@ -393,6 +427,22 @@ def test_month_range_union_tz(self): early_dr.union(late_dr) + def test_month_range_union_tz_dateutil(self): + _skip_if_no_dateutil() + from dateutil.tz import gettz as timezone + tz = timezone('US/Eastern') + + early_start = datetime(2011, 1, 1) + early_end = datetime(2011, 3, 1) + + late_start = datetime(2011, 3, 1) + late_end = datetime(2011, 5, 1) + + early_dr = date_range(start=early_start, end=early_end, tz=tz, freq=datetools.monthEnd) + late_dr = date_range(start=late_start, end=late_end, tz=tz, freq=datetools.monthEnd) + + early_dr.union(late_dr) + def test_range_closed(self): begin = datetime(2011, 1, 1) end = datetime(2014, 1, 1) @@ -580,6 +630,11 @@ def test_summary_pytz(self): import pytz cdate_range('1/1/2005', '1/1/2009', tz=pytz.utc).summary() + def test_summary_dateutil(self): + _skip_if_no_dateutil() + import dateutil + cdate_range('1/1/2005', '1/1/2009', tz=dateutil.tz.gettz('UTC')).summary() + def test_misc(self): end = datetime(2009, 5, 13) dr = cdate_range(end=end, periods=20) diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py index e0aea9a1a29b1..169939c2f288a 100644 --- a/pandas/tseries/tests/test_period.py +++ b/pandas/tseries/tests/test_period.py @@ -83,6 +83,16 @@ def test_timestamp_tz_arg(self): self.assertEqual(p.tz, pytz.timezone('Europe/Brussels').normalize(p).tzinfo) + def test_timestamp_tz_arg_dateutil(self): + import dateutil + p = Period('1/1/2005', freq='M').to_timestamp(tz=dateutil.tz.gettz('Europe/Brussels')) + self.assertEqual(p.tz, dateutil.tz.gettz('Europe/Brussels')) + + def test_timestamp_tz_arg_dateutil_from_string(self): + import dateutil + p = Period('1/1/2005', freq='M').to_timestamp(tz='dateutil/Europe/Brussels') + self.assertEqual(p.tz, dateutil.tz.gettz('Europe/Brussels')) + def test_period_constructor(self): i1 = Period('1/1/2005', freq='M') i2 = Period('Jan 2005') diff --git a/pandas/tseries/tests/test_timeseries.py b/pandas/tseries/tests/test_timeseries.py index 068883423015e..610b5687b9fdf 100644 --- a/pandas/tseries/tests/test_timeseries.py +++ b/pandas/tseries/tests/test_timeseries.py @@ -41,6 +41,12 @@ from numpy.testing.decorators import slow +def _skip_if_no_dateutil(): + try: + import dateutil + except ImportError: + raise nose.SkipTest("dateutil not installed") + def _skip_if_no_pytz(): try: import pytz @@ -400,6 +406,28 @@ def test_timestamp_to_datetime(self): self.assertEqual(stamp, dtval) self.assertEqual(stamp.tzinfo, dtval.tzinfo) + def test_timestamp_to_datetime_explicit_pytz(self): + _skip_if_no_pytz() + import pytz + rng = date_range('20090415', '20090519', + tz=pytz.timezone('US/Eastern')) + + stamp = rng[0] + dtval = stamp.to_pydatetime() + self.assertEquals(stamp, dtval) + self.assertEquals(stamp.tzinfo, dtval.tzinfo) + + def test_timestamp_to_datetime_explicit_dateutil(self): + _skip_if_no_dateutil() + import dateutil + rng = date_range('20090415', '20090519', + tz=dateutil.tz.gettz('US/Eastern')) + + stamp = rng[0] + dtval = stamp.to_pydatetime() + self.assertEquals(stamp, dtval) + self.assertEquals(stamp.tzinfo, dtval.tzinfo) + def test_index_convert_to_datetime_array(self): _skip_if_no_pytz() @@ -419,6 +447,46 @@ def _check_rng(rng): _check_rng(rng_eastern) _check_rng(rng_utc) + def test_index_convert_to_datetime_array_explicit_pytz(self): + _skip_if_no_pytz() + import pytz + + def _check_rng(rng): + converted = rng.to_pydatetime() + tm.assert_isinstance(converted, np.ndarray) + for x, stamp in zip(converted, rng): + tm.assert_isinstance(x, datetime) + self.assertEquals(x, stamp.to_pydatetime()) + self.assertEquals(x.tzinfo, stamp.tzinfo) + + rng = date_range('20090415', '20090519') + rng_eastern = date_range('20090415', '20090519', tz=pytz.timezone('US/Eastern')) + rng_utc = date_range('20090415', '20090519', tz=pytz.utc) + + _check_rng(rng) + _check_rng(rng_eastern) + _check_rng(rng_utc) + + def test_index_convert_to_datetime_array_explicit_dateutil(self): + _skip_if_no_dateutil() + import dateutil + + def _check_rng(rng): + converted = rng.to_pydatetime() + tm.assert_isinstance(converted, np.ndarray) + for x, stamp in zip(converted, rng): + tm.assert_isinstance(x, datetime) + self.assertEquals(x, stamp.to_pydatetime()) + self.assertEquals(x.tzinfo, stamp.tzinfo) + + rng = date_range('20090415', '20090519') + rng_eastern = date_range('20090415', '20090519', tz=dateutil.tz.gettz('US/Eastern')) + rng_utc = date_range('20090415', '20090519', tz=dateutil.tz.gettz('UTC')) + + _check_rng(rng) + _check_rng(rng_eastern) + _check_rng(rng_utc) + def test_ctor_str_intraday(self): rng = DatetimeIndex(['1-1-2000 00:00:01']) self.assertEqual(rng[0].second, 1) @@ -1430,7 +1498,7 @@ def test_to_period_microsecond(self): self.assertEqual(period[0], Period('2007-01-01 10:11:12.123456Z', 'U')) self.assertEqual(period[1], Period('2007-01-01 10:11:13.789123Z', 'U')) - def test_to_period_tz(self): + def test_to_period_tz_pytz(self): _skip_if_no_pytz() from dateutil.tz import tzlocal from pytz import utc as UTC @@ -1461,6 +1529,68 @@ def test_to_period_tz(self): self.assertEqual(result, expected) self.assertTrue(ts.to_period().equals(xp)) + def test_to_period_tz_explicit_pytz(self): + _skip_if_no_pytz() + import pytz + from dateutil.tz import tzlocal + + xp = date_range('1/1/2000', '4/1/2000').to_period() + + ts = date_range('1/1/2000', '4/1/2000', tz=pytz.timezone('US/Eastern')) + + result = ts.to_period()[0] + expected = ts[0].to_period() + + self.assert_(result == expected) + self.assert_(ts.to_period().equals(xp)) + + ts = date_range('1/1/2000', '4/1/2000', tz=pytz.utc) + + result = ts.to_period()[0] + expected = ts[0].to_period() + + self.assert_(result == expected) + self.assert_(ts.to_period().equals(xp)) + + ts = date_range('1/1/2000', '4/1/2000', tz=tzlocal()) + + result = ts.to_period()[0] + expected = ts[0].to_period() + + self.assert_(result == expected) + self.assert_(ts.to_period().equals(xp)) + + def test_to_period_tz_explicit_dateutil(self): + _skip_if_no_dateutil() + import dateutil + from dateutil.tz import tzlocal + + xp = date_range('1/1/2000', '4/1/2000').to_period() + + ts = date_range('1/1/2000', '4/1/2000', tz=dateutil.tz.gettz('US/Eastern')) + + result = ts.to_period()[0] + expected = ts[0].to_period() + + self.assert_(result == expected) + self.assert_(ts.to_period().equals(xp)) + + ts = date_range('1/1/2000', '4/1/2000', tz=dateutil.tz.gettz('UTC')) + + result = ts.to_period()[0] + expected = ts[0].to_period() + + self.assert_(result == expected) + self.assert_(ts.to_period().equals(xp)) + + ts = date_range('1/1/2000', '4/1/2000', tz=tzlocal()) + + result = ts.to_period()[0] + expected = ts[0].to_period() + + self.assert_(result == expected) + self.assert_(ts.to_period().equals(xp)) + def test_frame_to_period(self): K = 5 from pandas.tseries.period import period_range @@ -1639,6 +1769,54 @@ def test_append_concat_tz(self): appended = rng.append(rng2) self.assertTrue(appended.equals(rng3)) + def test_append_concat_tz_explicit_pytz(self): + # GH 2938 + _skip_if_no_pytz() + from pytz import timezone as timezone + + rng = date_range('5/8/2012 1:45', periods=10, freq='5T', + tz=timezone('US/Eastern')) + rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T', + tz=timezone('US/Eastern')) + rng3 = date_range('5/8/2012 1:45', periods=20, freq='5T', + tz=timezone('US/Eastern')) + ts = Series(np.random.randn(len(rng)), rng) + df = DataFrame(np.random.randn(len(rng), 4), index=rng) + ts2 = Series(np.random.randn(len(rng2)), rng2) + df2 = DataFrame(np.random.randn(len(rng2), 4), index=rng2) + + result = ts.append(ts2) + result_df = df.append(df2) + self.assert_(result.index.equals(rng3)) + self.assert_(result_df.index.equals(rng3)) + + appended = rng.append(rng2) + self.assert_(appended.equals(rng3)) + + def test_append_concat_tz_explicit_dateutil(self): + # GH 2938 + _skip_if_no_dateutil() + from dateutil.tz import gettz as timezone + + rng = date_range('5/8/2012 1:45', periods=10, freq='5T', + tz=timezone('US/Eastern')) + rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T', + tz=timezone('US/Eastern')) + rng3 = date_range('5/8/2012 1:45', periods=20, freq='5T', + tz=timezone('US/Eastern')) + ts = Series(np.random.randn(len(rng)), rng) + df = DataFrame(np.random.randn(len(rng), 4), index=rng) + ts2 = Series(np.random.randn(len(rng2)), rng2) + df2 = DataFrame(np.random.randn(len(rng2), 4), index=rng2) + + result = ts.append(ts2) + result_df = df.append(df2) + self.assert_(result.index.equals(rng3)) + self.assert_(result_df.index.equals(rng3)) + + appended = rng.append(rng2) + self.assert_(appended.equals(rng3)) + def test_set_dataframe_column_ns_dtype(self): x = DataFrame([datetime.now(), datetime.now()]) self.assertEqual(x[0].dtype, np.dtype('M8[ns]')) @@ -1817,7 +1995,7 @@ def test_period_resample(self): result2 = s.resample('T', kind='period') assert_series_equal(result2, expected) - def test_period_resample_with_local_timezone(self): + def test_period_resample_with_local_timezone_pytz(self): # GH5430 _skip_if_no_pytz() import pytz @@ -1838,6 +2016,28 @@ def test_period_resample_with_local_timezone(self): expected = pd.Series(1, index=expected_index) assert_series_equal(result, expected) + def test_period_resample_with_local_timezone_dateutil(self): + # GH5430 + _skip_if_no_dateutil() + import dateutil + + local_timezone = dateutil.tz.gettz('America/Los_Angeles') + + start = datetime(year=2013, month=11, day=1, hour=0, minute=0, tzinfo=dateutil.tz.gettz('UTC')) + # 1 day later + end = datetime(year=2013, month=11, day=2, hour=0, minute=0, tzinfo=dateutil.tz.gettz('UTC')) + + index = pd.date_range(start, end, freq='H') + + series = pd.Series(1, index=index) + series = series.tz_convert(local_timezone) + result = series.resample('D', kind='period') + # Create the expected series + expected_index = (pd.period_range(start=start, end=end, freq='D') - 1) # Index is moved back a day with the timezone conversion from UTC to Pacific + expected = pd.Series(1, index=expected_index) + assert_series_equal(result, expected) + + def test_pickle(self): #GH4606 from pandas.compat import cPickle @@ -2727,15 +2927,27 @@ def test_string_index_series_name_converted(self): class TestTimestamp(tm.TestCase): - def test_class_ops(self): + def test_class_ops_pytz(self): _skip_if_no_pytz() - import pytz + from pytz import timezone + + def compare(x, y): + self.assertEqual(int(Timestamp(x).value / 1e9), int(Timestamp(y).value / 1e9)) + + compare(Timestamp.now(), datetime.now()) + compare(Timestamp.now('UTC'), datetime.now(timezone('UTC'))) + compare(Timestamp.utcnow(), datetime.utcnow()) + compare(Timestamp.today(), datetime.today()) + + def test_class_ops_dateutil(self): + _skip_if_no_dateutil() + from dateutil.tz import gettz as timezone def compare(x,y): self.assertEqual(int(np.round(Timestamp(x).value/1e9)), int(np.round(Timestamp(y).value/1e9))) compare(Timestamp.now(),datetime.now()) - compare(Timestamp.now('UTC'),datetime.now(pytz.timezone('UTC'))) + compare(Timestamp.now('UTC'), datetime.now(timezone('UTC'))) compare(Timestamp.utcnow(),datetime.utcnow()) compare(Timestamp.today(),datetime.today()) @@ -2863,6 +3075,53 @@ def test_cant_compare_tz_naive_w_aware(self): self.assertFalse(a == b.to_pydatetime()) self.assertFalse(a.to_pydatetime() == b) + def test_cant_compare_tz_naive_w_aware_explicit_pytz(self): + _skip_if_no_pytz() + from pytz import utc + # #1404 + a = Timestamp('3/12/2012') + b = Timestamp('3/12/2012', tz=utc) + + self.assertRaises(Exception, a.__eq__, b) + self.assertRaises(Exception, a.__ne__, b) + self.assertRaises(Exception, a.__lt__, b) + self.assertRaises(Exception, a.__gt__, b) + self.assertRaises(Exception, b.__eq__, a) + self.assertRaises(Exception, b.__ne__, a) + self.assertRaises(Exception, b.__lt__, a) + self.assertRaises(Exception, b.__gt__, a) + + if sys.version_info < (3, 3): + self.assertRaises(Exception, a.__eq__, b.to_pydatetime()) + self.assertRaises(Exception, a.to_pydatetime().__eq__, b) + else: + self.assertFalse(a == b.to_pydatetime()) + self.assertFalse(a.to_pydatetime() == b) + + def test_cant_compare_tz_naive_w_aware_dateutil(self): + _skip_if_no_dateutil() + from dateutil.tz import gettz + utc = gettz('UTC') + # #1404 + a = Timestamp('3/12/2012') + b = Timestamp('3/12/2012', tz=utc) + + self.assertRaises(Exception, a.__eq__, b) + self.assertRaises(Exception, a.__ne__, b) + self.assertRaises(Exception, a.__lt__, b) + self.assertRaises(Exception, a.__gt__, b) + self.assertRaises(Exception, b.__eq__, a) + self.assertRaises(Exception, b.__ne__, a) + self.assertRaises(Exception, b.__lt__, a) + self.assertRaises(Exception, b.__gt__, a) + + if sys.version_info < (3, 3): + self.assertRaises(Exception, a.__eq__, b.to_pydatetime()) + self.assertRaises(Exception, a.to_pydatetime().__eq__, b) + else: + self.assertFalse(a == b.to_pydatetime()) + self.assertFalse(a.to_pydatetime() == b) + def test_delta_preserve_nanos(self): val = Timestamp(long(1337299200000000123)) result = val + timedelta(1) diff --git a/pandas/tseries/tests/test_timezones.py b/pandas/tseries/tests/test_timezones.py index 9514d5ca6e02c..b3ae02320037c 100644 --- a/pandas/tseries/tests/test_timezones.py +++ b/pandas/tseries/tests/test_timezones.py @@ -2,6 +2,7 @@ from datetime import datetime, time, timedelta, tzinfo, date import sys import os +import unittest import nose import numpy as np @@ -11,6 +12,7 @@ date_range, Timestamp) from pandas import DatetimeIndex, Int64Index, to_datetime, NaT +from pandas import tslib import pandas.core.datetools as datetools import pandas.tseries.offsets as offsets @@ -38,11 +40,22 @@ def _skip_if_no_pytz(): except ImportError: raise nose.SkipTest("pytz not installed") +def _skip_if_no_dateutil(): + try: + import dateutil + except ImportError: + raise nose.SkipTest + try: import pytz except ImportError: pass +try: + import dateutil +except ImportError: + pass + class FixedOffset(tzinfo): """Fixed offset in minutes east from UTC.""" @@ -64,20 +77,44 @@ def dst(self, dt): fixed_off_no_name = FixedOffset(-330, None) -class TestTimeZoneSupport(tm.TestCase): +class TestTimeZoneSupportPytz(tm.TestCase): _multiprocess_can_split_ = True def setUp(self): _skip_if_no_pytz() + def tz(self, tz): + ''' Construct a timezone object from a string. Overridden in subclass to parameterize tests. ''' + return pytz.timezone(tz) + + def tzstr(self, tz): + ''' Construct a timezone string from a string. Overridden in subclass to parameterize tests. ''' + return tz + + def localize(self, tz, x): + return tz.localize(x) + + def cmptz(self, tz1, tz2): + ''' Compare two timezones. Overridden in subclass to parameterize tests. ''' + return tz1.zone == tz2.zone + def test_utc_to_local_no_modify(self): rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert('US/Eastern') + rng_eastern = rng.tz_convert(self.tzstr('US/Eastern')) + + # Values are unmodified + self.assert_(np.array_equal(rng.asi8, rng_eastern.asi8)) + + self.assert_(self.cmptz(rng_eastern.tz, self.tz('US/Eastern'))) + + def test_utc_to_local_no_modify_explicit(self): + rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc') + rng_eastern = rng.tz_convert(self.tz('US/Eastern')) # Values are unmodified self.assert_numpy_array_equal(rng.asi8, rng_eastern.asi8) - self.assertEqual(rng_eastern.tz, pytz.timezone('US/Eastern')) + self.assertEqual(rng_eastern.tz, self.tz('US/Eastern')) def test_localize_utc_conversion(self): @@ -87,20 +124,43 @@ def test_localize_utc_conversion(self): rng = date_range('3/10/2012', '3/11/2012', freq='30T') - converted = rng.tz_localize('US/Eastern') + converted = rng.tz_localize(self.tzstr('US/Eastern')) expected_naive = rng + offsets.Hour(5) self.assert_numpy_array_equal(converted.asi8, expected_naive.asi8) # DST ambiguity, this should fail rng = date_range('3/11/2012', '3/12/2012', freq='30T') # Is this really how it should fail?? - self.assertRaises(NonExistentTimeError, rng.tz_localize, 'US/Eastern') + self.assertRaises(NonExistentTimeError, rng.tz_localize, self.tzstr('US/Eastern')) + + def test_localize_utc_conversion_explicit(self): + # Localizing to time zone should: + # 1) check for DST ambiguities + # 2) convert to UTC + + rng = date_range('3/10/2012', '3/11/2012', freq='30T') + converted = rng.tz_localize(self.tz('US/Eastern')) + expected_naive = rng + offsets.Hour(5) + self.assert_(np.array_equal(converted.asi8, expected_naive.asi8)) + + # DST ambiguity, this should fail + rng = date_range('3/11/2012', '3/12/2012', freq='30T') + # Is this really how it should fail?? + self.assertRaises(NonExistentTimeError, rng.tz_localize, self.tz('US/Eastern')) def test_timestamp_tz_localize(self): stamp = Timestamp('3/11/2012 04:00') - result = stamp.tz_localize('US/Eastern') - expected = Timestamp('3/11/2012 04:00', tz='US/Eastern') + result = stamp.tz_localize(self.tzstr('US/Eastern')) + expected = Timestamp('3/11/2012 04:00', tz=self.tzstr('US/Eastern')) + self.assertEqual(result.hour, expected.hour) + self.assertEqual(result, expected) + + def test_timestamp_tz_localize_explicit(self): + stamp = Timestamp('3/11/2012 04:00') + + result = stamp.tz_localize(self.tz('US/Eastern')) + expected = Timestamp('3/11/2012 04:00', tz=self.tz('US/Eastern')) self.assertEqual(result.hour, expected.hour) self.assertEqual(result, expected) @@ -108,12 +168,22 @@ def test_timestamp_constructed_by_date_and_tz(self): # Fix Issue 2993, Timestamp cannot be constructed by datetime.date # and tz correctly - result = Timestamp(date(2012, 3, 11), tz='US/Eastern') + result = Timestamp(date(2012, 3, 11), tz=self.tzstr('US/Eastern')) - expected = Timestamp('3/11/2012', tz='US/Eastern') + expected = Timestamp('3/11/2012', tz=self.tzstr('US/Eastern')) self.assertEqual(result.hour, expected.hour) self.assertEqual(result, expected) + def test_timestamp_constructed_by_date_and_tz_explicit(self): + # Fix Issue 2993, Timestamp cannot be constructed by datetime.date + # and tz correctly + + result = Timestamp(date(2012, 3, 11), tz=self.tz('US/Eastern')) + + expected = Timestamp('3/11/2012', tz=self.tz('US/Eastern')) + self.assertEquals(result.hour, expected.hour) + self.assertEquals(result, expected) + def test_timestamp_to_datetime_tzoffset(self): # tzoffset from dateutil.tz import tzoffset @@ -126,12 +196,25 @@ def test_timedelta_push_over_dst_boundary(self): # #1389 # 4 hours before DST transition - stamp = Timestamp('3/10/2012 22:00', tz='US/Eastern') + stamp = Timestamp('3/10/2012 22:00', tz=self.tzstr('US/Eastern')) result = stamp + timedelta(hours=6) # spring forward, + "7" hours - expected = Timestamp('3/11/2012 05:00', tz='US/Eastern') + expected = Timestamp('3/11/2012 05:00', tz=self.tzstr('US/Eastern')) + + self.assertEquals(result, expected) + + def test_timedelta_push_over_dst_boundary_explicit(self): + # #1389 + + # 4 hours before DST transition + stamp = Timestamp('3/10/2012 22:00', tz=self.tz('US/Eastern')) + + result = stamp + timedelta(hours=6) + + # spring forward, + "7" hours + expected = Timestamp('3/11/2012 05:00', tz=self.tz('US/Eastern')) self.assertEqual(result, expected) @@ -140,7 +223,7 @@ def test_tz_localize_dti(self): dti = DatetimeIndex(start='1/1/2005', end='1/1/2005 0:00:30.256', freq='L') - dti2 = dti.tz_localize('US/Eastern') + dti2 = dti.tz_localize(self.tzstr('US/Eastern')) dti_utc = DatetimeIndex(start='1/1/2005 05:00', end='1/1/2005 5:00:30.256', freq='L', @@ -148,18 +231,18 @@ def test_tz_localize_dti(self): self.assert_numpy_array_equal(dti2.values, dti_utc.values) - dti3 = dti2.tz_convert('US/Pacific') + dti3 = dti2.tz_convert(self.tzstr('US/Pacific')) self.assert_numpy_array_equal(dti3.values, dti_utc.values) dti = DatetimeIndex(start='11/6/2011 1:59', end='11/6/2011 2:00', freq='L') self.assertRaises(pytz.AmbiguousTimeError, dti.tz_localize, - 'US/Eastern') + self.tzstr('US/Eastern')) dti = DatetimeIndex(start='3/13/2011 1:59', end='3/13/2011 2:00', freq='L') self.assertRaises( - pytz.NonExistentTimeError, dti.tz_localize, 'US/Eastern') + pytz.NonExistentTimeError, dti.tz_localize, self.tzstr('US/Eastern')) def test_tz_localize_empty_series(self): # #2248 @@ -169,22 +252,22 @@ def test_tz_localize_empty_series(self): ts2 = ts.tz_localize('utc') self.assertTrue(ts2.index.tz == pytz.utc) - ts2 = ts.tz_localize('US/Eastern') - self.assertTrue(ts2.index.tz == pytz.timezone('US/Eastern')) + ts2 = ts.tz_localize(self.tzstr('US/Eastern')) + self.assertTrue(self.cmptz(ts2.index.tz, self.tz('US/Eastern'))) def test_astimezone(self): utc = Timestamp('3/11/2012 22:00', tz='UTC') - expected = utc.tz_convert('US/Eastern') - result = utc.astimezone('US/Eastern') + expected = utc.tz_convert(self.tzstr('US/Eastern')) + result = utc.astimezone(self.tzstr('US/Eastern')) self.assertEqual(expected, result) tm.assert_isinstance(result, Timestamp) def test_create_with_tz(self): - stamp = Timestamp('3/11/2012 05:00', tz='US/Eastern') + stamp = Timestamp('3/11/2012 05:00', tz=self.tzstr('US/Eastern')) self.assertEqual(stamp.hour, 5) rng = date_range( - '3/11/2012 04:00', periods=10, freq='H', tz='US/Eastern') + '3/11/2012 04:00', periods=10, freq='H', tz=self.tzstr('US/Eastern')) self.assertEqual(stamp, rng[1]) @@ -257,10 +340,10 @@ def test_date_range_localize(self): def test_utc_box_timestamp_and_localize(self): rng = date_range('3/11/2012', '3/12/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert('US/Eastern') + rng_eastern = rng.tz_convert(self.tzstr('US/Eastern')) - tz = pytz.timezone('US/Eastern') - expected = tz.normalize(rng[-1]) + tz = self.tz('US/Eastern') + expected = rng[-1].astimezone(tz) stamp = rng_eastern[-1] self.assertEqual(stamp, expected) @@ -268,15 +351,17 @@ def test_utc_box_timestamp_and_localize(self): # right tzinfo rng = date_range('3/13/2012', '3/14/2012', freq='H', tz='utc') - rng_eastern = rng.tz_convert('US/Eastern') - self.assertIn('EDT', repr(rng_eastern[0].tzinfo)) + rng_eastern = rng.tz_convert(self.tzstr('US/Eastern')) + # test not valid for dateutil timezones. + # self.assertIn('EDT', repr(rng_eastern[0].tzinfo)) + self.assert_('EDT' in repr(rng_eastern[0].tzinfo) or 'tzfile' in repr(rng_eastern[0].tzinfo)) def test_timestamp_tz_convert(self): strdates = ['1/1/2012', '3/1/2012', '4/1/2012'] - idx = DatetimeIndex(strdates, tz='US/Eastern') + idx = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern')) - conv = idx[0].tz_convert('US/Pacific') - expected = idx.tz_convert('US/Pacific')[0] + conv = idx[0].tz_convert(self.tzstr('US/Pacific')) + expected = idx.tz_convert(self.tzstr('US/Pacific'))[0] self.assertEqual(conv, expected) @@ -284,27 +369,27 @@ def test_pass_dates_localize_to_utc(self): strdates = ['1/1/2012', '3/1/2012', '4/1/2012'] idx = DatetimeIndex(strdates) - conv = idx.tz_localize('US/Eastern') + conv = idx.tz_localize(self.tzstr('US/Eastern')) - fromdates = DatetimeIndex(strdates, tz='US/Eastern') + fromdates = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern')) self.assertEqual(conv.tz, fromdates.tz) self.assert_numpy_array_equal(conv.values, fromdates.values) def test_field_access_localize(self): strdates = ['1/1/2012', '3/1/2012', '4/1/2012'] - rng = DatetimeIndex(strdates, tz='US/Eastern') + rng = DatetimeIndex(strdates, tz=self.tzstr('US/Eastern')) self.assertTrue((rng.hour == 0).all()) # a more unusual time zone, #1946 dr = date_range('2011-10-02 00:00', freq='h', periods=10, - tz='America/Atikokan') + tz=self.tzstr('America/Atikokan')) expected = np.arange(10) self.assert_numpy_array_equal(dr.hour, expected) def test_with_tz(self): - tz = pytz.timezone('US/Central') + tz = self.tz('US/Central') # just want it to work start = datetime(2011, 3, 12, tzinfo=pytz.utc) @@ -317,10 +402,11 @@ def test_with_tz(self): # normalized central = dr.tz_convert(tz) + self.assertIs(central.tz, tz) + self.assertIs(central[0].tz, tz) # compare vs a localized tz - comp = tz.localize(dr[0].to_pydatetime().replace(tzinfo=None)).tzinfo - self.assertIs(central.tz, tz) + comp = self.localize(tz, dr[0].to_pydatetime().replace(tzinfo=None)).tzinfo self.assertIs(central[0].tz, comp) # datetimes with tzinfo set @@ -338,9 +424,7 @@ def test_tz_localize(self): self.assert_numpy_array_equal(dr_utc, localized) def test_with_tz_ambiguous_times(self): - tz = pytz.timezone('US/Eastern') - - rng = bdate_range(datetime(2009, 1, 1), datetime(2010, 1, 1)) + tz = self.tz('US/Eastern') # March 13, 2011, spring forward, skip from 2 AM to 3 AM dr = date_range(datetime(2011, 3, 13, 1, 30), periods=3, @@ -363,7 +447,7 @@ def test_with_tz_ambiguous_times(self): def test_infer_dst(self): # November 6, 2011, fall back, repeat 2 AM hour # With no repeated hours, we cannot infer the transition - tz = pytz.timezone('US/Eastern') + tz = self.tz('US/Eastern') dr = date_range(datetime(2011, 11, 6, 0), periods=5, freq=datetools.Hour()) self.assertRaises(pytz.AmbiguousTimeError, dr.tz_localize, @@ -388,36 +472,36 @@ def test_infer_dst(self): # test utility methods def test_infer_tz(self): - eastern = pytz.timezone('US/Eastern') + eastern = self.tz('US/Eastern') utc = pytz.utc _start = datetime(2001, 1, 1) _end = datetime(2009, 1, 1) - start = eastern.localize(_start) - end = eastern.localize(_end) - assert(tools._infer_tzinfo(start, end) is eastern.localize(_start).tzinfo) - assert(tools._infer_tzinfo(start, None) is eastern.localize(_start).tzinfo) - assert(tools._infer_tzinfo(None, end) is eastern.localize(_end).tzinfo) + start = self.localize(eastern, _start) + end = self.localize(eastern, _end) + assert(tools._infer_tzinfo(start, end) is self.localize(eastern, _start).tzinfo) + assert(tools._infer_tzinfo(start, None) is self.localize(eastern, _start).tzinfo) + assert(tools._infer_tzinfo(None, end) is self.localize(eastern, _end).tzinfo) start = utc.localize(_start) end = utc.localize(_end) assert(tools._infer_tzinfo(start, end) is utc) - end = eastern.localize(_end) + end = self.localize(eastern, _end) self.assertRaises(Exception, tools._infer_tzinfo, start, end) self.assertRaises(Exception, tools._infer_tzinfo, end, start) def test_tz_string(self): - result = date_range('1/1/2000', periods=10, tz='US/Eastern') + result = date_range('1/1/2000', periods=10, tz=self.tzstr('US/Eastern')) expected = date_range('1/1/2000', periods=10, - tz=pytz.timezone('US/Eastern')) + tz=self.tz('US/Eastern')) self.assertTrue(result.equals(expected)) def test_take_dont_lose_meta(self): _skip_if_no_pytz() - rng = date_range('1/1/2000', periods=20, tz='US/Eastern') + rng = date_range('1/1/2000', periods=20, tz=self.tzstr('US/Eastern')) result = rng.take(lrange(5)) self.assertEqual(result.tz, rng.tz) @@ -426,7 +510,7 @@ def test_take_dont_lose_meta(self): def test_index_with_timezone_repr(self): rng = date_range('4/13/2010', '5/6/2010') - rng_eastern = rng.tz_localize('US/Eastern') + rng_eastern = rng.tz_localize(self.tzstr('US/Eastern')) rng_repr = repr(rng_eastern) self.assertIn('2010-04-13 00:00:00', rng_repr) @@ -435,7 +519,7 @@ def test_index_astype_asobject_tzinfos(self): # #1345 # dates around a dst transition - rng = date_range('2/13/2010', '5/6/2010', tz='US/Eastern') + rng = date_range('2/13/2010', '5/6/2010', tz=self.tzstr('US/Eastern')) objs = rng.asobject for i, x in enumerate(objs): @@ -455,21 +539,21 @@ def test_localized_at_time_between_time(self): rng = date_range('4/16/2012', '5/1/2012', freq='H') ts = Series(np.random.randn(len(rng)), index=rng) - ts_local = ts.tz_localize('US/Eastern') + ts_local = ts.tz_localize(self.tzstr('US/Eastern')) result = ts_local.at_time(time(10, 0)) - expected = ts.at_time(time(10, 0)).tz_localize('US/Eastern') + expected = ts.at_time(time(10, 0)).tz_localize(self.tzstr('US/Eastern')) assert_series_equal(result, expected) - self.assertEqual(result.index.tz.zone, 'US/Eastern') + self.assertTrue(self.cmptz(result.index.tz, self.tz('US/Eastern'))) t1, t2 = time(10, 0), time(11, 0) result = ts_local.between_time(t1, t2) - expected = ts.between_time(t1, t2).tz_localize('US/Eastern') + expected = ts.between_time(t1, t2).tz_localize(self.tzstr('US/Eastern')) assert_series_equal(result, expected) - self.assertEqual(result.index.tz.zone, 'US/Eastern') + self.assertTrue(self.cmptz(result.index.tz, self.tz('US/Eastern'))) def test_string_index_alias_tz_aware(self): - rng = date_range('1/1/2000', periods=10, tz='US/Eastern') + rng = date_range('1/1/2000', periods=10, tz=self.tzstr('US/Eastern')) ts = Series(np.random.randn(len(rng)), index=rng) result = ts['1/3/2000'] @@ -494,14 +578,14 @@ def test_fixedtz_topydatetime(self): def test_convert_tz_aware_datetime_datetime(self): # #1581 - tz = pytz.timezone('US/Eastern') + tz = self.tz('US/Eastern') dates = [datetime(2000, 1, 1), datetime(2000, 1, 2), datetime(2000, 1, 3)] - dates_aware = [tz.localize(x) for x in dates] + dates_aware = [self.localize(tz, x) for x in dates] result = to_datetime(dates_aware) - self.assertEqual(result.tz.zone, 'US/Eastern') + self.assertTrue(self.cmptz(result.tz, self.tz('US/Eastern'))) converted = to_datetime(dates_aware, utc=True) ex_vals = [Timestamp(x).value for x in dates_aware] @@ -534,7 +618,7 @@ def test_to_datetime_tzlocal(self): def test_frame_no_datetime64_dtype(self): dr = date_range('2011/1/1', '2012/1/1', freq='W-FRI') - dr_tz = dr.tz_localize('US/Eastern') + dr_tz = dr.tz_localize(self.tzstr('US/Eastern')) e = DataFrame({'A': 'foo', 'B': dr_tz}, index=dr) self.assertEqual(e['B'].dtype, 'M8[ns]') @@ -558,7 +642,7 @@ def test_hongkong_tz_convert(self): def test_tz_convert_unsorted(self): dr = date_range('2012-03-09', freq='H', periods=100, tz='utc') - dr = dr.tz_convert('US/Eastern') + dr = dr.tz_convert(self.tzstr('US/Eastern')) result = dr[::-1].hour exp = dr.hour[::-1] @@ -566,14 +650,14 @@ def test_tz_convert_unsorted(self): def test_shift_localized(self): dr = date_range('2011/1/1', '2012/1/1', freq='W-FRI') - dr_tz = dr.tz_localize('US/Eastern') + dr_tz = dr.tz_localize(self.tzstr('US/Eastern')) result = dr_tz.shift(1, '10T') self.assertEqual(result.tz, dr_tz.tz) def test_tz_aware_asfreq(self): dr = date_range( - '2011-12-01', '2012-07-20', freq='D', tz='US/Eastern') + '2011-12-01', '2012-07-20', freq='D', tz=self.tzstr('US/Eastern')) s = Series(np.random.randn(len(dr)), index=dr) @@ -582,15 +666,15 @@ def test_tz_aware_asfreq(self): def test_static_tzinfo(self): # it works! - index = DatetimeIndex([datetime(2012, 1, 1)], tz='EST') + index = DatetimeIndex([datetime(2012, 1, 1)], tz=self.tzstr('EST')) index.hour index[0] def test_tzaware_datetime_to_index(self): - d = [datetime(2012, 8, 19, tzinfo=pytz.timezone('US/Eastern'))] + d = [datetime(2012, 8, 19, tzinfo=self.tz('US/Eastern'))] index = DatetimeIndex(d) - self.assertEqual(index.tz.zone, 'US/Eastern') + self.assertTrue(self.cmptz(index.tz, self.tz('US/Eastern'))) def test_date_range_span_dst_transition(self): # #1778 @@ -601,11 +685,11 @@ def test_date_range_span_dst_transition(self): self.assertTrue((dr.hour == 0).all()) - dr = date_range('2012-11-02', periods=10, tz='US/Eastern') + dr = date_range('2012-11-02', periods=10, tz=self.tzstr('US/Eastern')) self.assertTrue((dr.hour == 0).all()) def test_convert_datetime_list(self): - dr = date_range('2012-06-02', periods=10, tz='US/Eastern') + dr = date_range('2012-06-02', periods=10, tz=self.tzstr('US/Eastern')) dr2 = DatetimeIndex(list(dr), name='foo') self.assertTrue(dr.equals(dr2)) @@ -620,7 +704,7 @@ def test_frame_from_records_utc(self): DataFrame.from_records([rec], index='begin_time') def test_frame_reset_index(self): - dr = date_range('2012-06-02', periods=10, tz='US/Eastern') + dr = date_range('2012-06-02', periods=10, tz=self.tzstr('US/Eastern')) df = DataFrame(np.random.randn(len(dr)), dr) roundtripped = df.reset_index().set_index('index') xp = df.index.tz @@ -643,10 +727,10 @@ def test_dateutil_tzoffset_support(self): def test_getitem_pydatetime_tz(self): index = date_range(start='2012-12-24 16:00', end='2012-12-24 18:00', freq='H', - tz='Europe/Berlin') + tz=self.tzstr('Europe/Berlin')) ts = Series(index=index, data=index.hour) - time_pandas = Timestamp('2012-12-24 17:00', tz='Europe/Berlin') - time_datetime = pytz.timezone('Europe/Berlin').localize(datetime(2012, 12, 24, 17, 0)) + time_pandas = Timestamp('2012-12-24 17:00', tz=self.tzstr('Europe/Berlin')) + time_datetime = self.localize(self.tz('Europe/Berlin'), datetime(2012, 12, 24, 17, 0)) self.assertEqual(ts[time_pandas], ts[time_datetime]) def test_index_drop_dont_lose_tz(self): @@ -663,21 +747,43 @@ def test_datetimeindex_tz(self): arr = ['11/10/2005 08:00:00', '11/10/2005 09:00:00'] - idx1 = to_datetime(arr).tz_localize('US/Eastern') - idx2 = DatetimeIndex(start="2005-11-10 08:00:00", freq='H', periods=2, tz='US/Eastern') - idx3 = DatetimeIndex(arr, tz='US/Eastern') - idx4 = DatetimeIndex(np.array(arr), tz='US/Eastern') + idx1 = to_datetime(arr).tz_localize(self.tzstr('US/Eastern')) + idx2 = DatetimeIndex(start="2005-11-10 08:00:00", freq='H', periods=2, tz=self.tzstr('US/Eastern')) + idx3 = DatetimeIndex(arr, tz=self.tzstr('US/Eastern')) + idx4 = DatetimeIndex(np.array(arr), tz=self.tzstr('US/Eastern')) for other in [idx2, idx3, idx4]: self.assertTrue(idx1.equals(other)) def test_datetimeindex_tz_nat(self): - idx = to_datetime([Timestamp("2013-1-1", tz='US/Eastern'), NaT]) + idx = to_datetime([Timestamp("2013-1-1", tz=self.tzstr('US/Eastern')), NaT]) self.assertTrue(isnull(idx[1])) self.assertTrue(idx[0].tzinfo is not None) +class TestTimeZoneSupportDateutil(TestTimeZoneSupportPytz): + _multiprocess_can_split_ = True + + def setUp(self): + _skip_if_no_dateutil() + + def tz(self, tz): + ''' Construct a timezone object from a string. Overridden in subclass to parameterize tests. ''' + return dateutil.tz.gettz(tz) + + def tzstr(self, tz): + ''' Construct a timezone string from a string. Overridden in subclass to parameterize tests. ''' + return 'dateutil/' + tz + + def cmptz(self, tz1, tz2): + ''' Compare two timezones. Overridden in subclass to parameterize tests. ''' + return tz1 == tz2 + + def localize(self, tz, x): + return x.replace(tzinfo=tz) + + class TestTimeZones(tm.TestCase): _multiprocess_can_split_ = True diff --git a/pandas/tseries/tools.py b/pandas/tseries/tools.py index 4260705eadb03..f8043b23a58af 100644 --- a/pandas/tseries/tools.py +++ b/pandas/tseries/tools.py @@ -57,9 +57,7 @@ def _infer(a, b): def _maybe_get_tz(tz, date=None): - if isinstance(tz, compat.string_types): - import pytz - tz = pytz.timezone(tz) + tz = tslib.maybe_get_tz(tz) if com.is_integer(tz): import pytz tz = pytz.FixedOffset(tz / 60) @@ -71,6 +69,7 @@ def _maybe_get_tz(tz, date=None): return tz + def _guess_datetime_format(dt_str, dayfirst=False, dt_str_parse=compat.parse_date, dt_str_split=_DATEUTIL_LEXER_SPLIT): diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index df9c465c33853..e7385400e5962 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -34,8 +34,11 @@ cimport cython from datetime import timedelta, datetime from datetime import time as datetime_time -from dateutil.tz import tzoffset +from dateutil.tz import (tzoffset, tzlocal as _dateutil_tzlocal, tzfile as _dateutil_tzfile, + tzutc as _dateutil_tzutc, gettz as _dateutil_gettz) +from pytz.tzinfo import BaseTzInfo as _pytz_BaseTzInfo from pandas.compat import parse_date +from pandas.compat import parse_date, string_types from sys import version_info @@ -105,14 +108,19 @@ def ints_to_pydatetime(ndarray[int64_t] arr, tz=None): else: # Adjust datetime64 timestamp, recompute datetimestruct - pos = trans.searchsorted(arr[i]) - 1 - inf = tz._transition_info[pos] + pos = trans.searchsorted(arr[i], side='right') - 1 + if _treat_tz_as_pytz(tz): + # find right representation of dst etc in pytz timezone + new_tz = tz._tzinfos[tz._transition_info[pos]] + else: + # no zone-name change for dateutil tzs - dst etc represented in single object. + new_tz = tz pandas_datetime_to_datetimestruct(arr[i] + deltas[pos], PANDAS_FR_ns, &dts) result[i] = datetime(dts.year, dts.month, dts.day, dts.hour, dts.min, dts.sec, dts.us, - tz._tzinfos[inf]) + new_tz) else: for i in range(n): if arr[i] == iNaT: @@ -124,17 +132,23 @@ def ints_to_pydatetime(ndarray[int64_t] arr, tz=None): return result -from dateutil.tz import tzlocal -def _is_tzlocal(tz): - return isinstance(tz, tzlocal) +cdef inline bint _is_tzlocal(object tz): + return isinstance(tz, _dateutil_tzlocal) -def _is_fixed_offset(tz): - try: - tz._transition_info - return False - except AttributeError: - return True +cdef inline bint _is_fixed_offset(object tz): + if _treat_tz_as_dateutil(tz): + if len(tz._trans_idx) == 0 and len(tz._trans_list) == 0: + return 1 + else: + return 0 + elif _treat_tz_as_pytz(tz): + if len(tz._transition_info) == 0 and len(tz._utc_transition_times) == 0: + return 1 + else: + return 0 + return 1 + _zero_time = datetime_time(0, 0) @@ -157,7 +171,7 @@ class Timestamp(_Timestamp): def now(cls, tz=None): """ compat now with datetime """ if isinstance(tz, basestring): - tz = pytz.timezone(tz) + tz = maybe_get_tz(tz) return cls(datetime.now(tz)) @classmethod @@ -333,7 +347,7 @@ class Timestamp(_Timestamp): Parameters ---------- - tz : pytz.timezone + tz : pytz.timezone or dateutil.tz.tzfile Returns ------- @@ -353,7 +367,7 @@ class Timestamp(_Timestamp): Parameters ---------- - tz : pytz.timezone + tz : pytz.timezone or dateutil.tz.tzfile Returns ------- @@ -866,8 +880,7 @@ cdef convert_to_tsobject(object ts, object tz, object unit): bint utc_convert = 1 if tz is not None: - if isinstance(tz, basestring): - tz = pytz.timezone(tz) + tz = maybe_get_tz(tz) obj = _TSObject() @@ -954,6 +967,9 @@ cdef convert_to_tsobject(object ts, object tz, object unit): return obj cdef inline void _localize_tso(_TSObject obj, object tz): + ''' + Take a TSObject in UTC and localizes to timezone tz. + ''' if _is_utc(tz): obj.tzinfo = tz elif _is_tzlocal(tz): @@ -970,35 +986,75 @@ cdef inline void _localize_tso(_TSObject obj, object tz): deltas = _get_deltas(tz) pos = trans.searchsorted(obj.value, side='right') - 1 - # statictzinfo - if not hasattr(tz, '_transition_info'): - pandas_datetime_to_datetimestruct(obj.value + deltas[0], - PANDAS_FR_ns, &obj.dts) + + # static/pytz/dateutil specific code + if _is_fixed_offset(tz): + # statictzinfo + if len(deltas) > 0: + pandas_datetime_to_datetimestruct(obj.value + deltas[0], + PANDAS_FR_ns, &obj.dts) + else: + pandas_datetime_to_datetimestruct(obj.value, PANDAS_FR_ns, &obj.dts) obj.tzinfo = tz - else: + elif _treat_tz_as_pytz(tz): inf = tz._transition_info[pos] pandas_datetime_to_datetimestruct(obj.value + deltas[pos], PANDAS_FR_ns, &obj.dts) obj.tzinfo = tz._tzinfos[inf] + elif _treat_tz_as_dateutil(tz): + pandas_datetime_to_datetimestruct(obj.value + deltas[pos], + PANDAS_FR_ns, &obj.dts) + obj.tzinfo = tz + else: + obj.tzinfo = tz def get_timezone(tz): return _get_zone(tz) cdef inline bint _is_utc(object tz): - return tz is UTC or isinstance(tz, _du_utc) + return tz is UTC or isinstance(tz, _dateutil_tzutc) cdef inline object _get_zone(object tz): + ''' + We need to do several things here: + 1/ Distinguish between pytz and dateutil timezones + 2/ Not be over-specific (e.g. US/Eastern with/without DST is same *zone* but a different tz object) + 3/ Provide something to serialize when we're storing a datetime object in pytables. + + We return a string prefaced with dateutil if it's a dateutil tz, else just the tz name. It needs to be a + string so that we can serialize it with UJSON/pytables. maybe_get_tz (below) is the inverse of this process. + ''' if _is_utc(tz): return 'UTC' else: - try: - zone = tz.zone - if zone is None: + if _treat_tz_as_dateutil(tz): + return 'dateutil/' + tz._filename.split('zoneinfo/')[1] + else: + # tz is a pytz timezone or unknown. + try: + zone = tz.zone + if zone is None: + return tz + return zone + except AttributeError: return tz - return zone - except AttributeError: - return tz + + +cpdef inline object maybe_get_tz(object tz): + ''' + (Maybe) Construct a timezone object from a string. If tz is a string, use it to construct a timezone object. + Otherwise, just return tz. + ''' + if isinstance(tz, string_types): + split_tz = tz.split('/', 1) + if split_tz[0] == 'dateutil': + tz = _dateutil_gettz(split_tz[1]) + else: + tz = pytz.timezone(tz) + return tz + else: + return tz class OutOfBoundsDatetime(ValueError): @@ -1747,7 +1803,6 @@ def i8_to_pydt(int64_t i8, object tzinfo = None): # time zone conversion helpers try: - from dateutil.tz import tzutc as _du_utc import pytz UTC = pytz.utc have_pytz = True @@ -1884,22 +1939,48 @@ def tz_convert_single(int64_t val, object tz1, object tz2): offset = deltas[pos] return utc_date + offset - +# Timezone data caches, key is the pytz string or dateutil file name. trans_cache = {} utc_offset_cache = {} -def _get_transitions(tz): +cdef inline bint _treat_tz_as_pytz(object tz): + return hasattr(tz, '_utc_transition_times') and hasattr(tz, '_transition_info') + +cdef inline bint _treat_tz_as_dateutil(object tz): + return hasattr(tz, '_trans_list') and hasattr(tz, '_trans_idx') + + +cdef inline object _tz_cache_key(object tz): + """ + Return the key in the cache for the timezone info object or None if unknown. + + The key is currently the tz string for pytz timezones, the filename for dateutil timezones. + + Notes + ===== + This cannot just be the hash of a timezone object. Unfortunately, the hashes of two dateutil tz objects + which represent the same timezone are not equal (even though the tz objects will compare equal and + represent the same tz file). + Also, pytz objects are not always hashable so we use str(tz) instead. + """ + if isinstance(tz, _pytz_BaseTzInfo): + return tz.zone + elif isinstance(tz, _dateutil_tzfile): + return tz._filename + else: + return None + + +cdef object _get_transitions(object tz): """ Get UTC times of DST transitions """ - try: - # tzoffset not hashable in Python 3 - hash(tz) - except TypeError: + cache_key = _tz_cache_key(tz) + if cache_key is None: return np.array([NPY_NAT + 1], dtype=np.int64) - if tz not in trans_cache: - if hasattr(tz, '_utc_transition_times'): + if cache_key not in trans_cache: + if _treat_tz_as_pytz(tz): arr = np.array(tz._utc_transition_times, dtype='M8[ns]') arr = arr.view('i8') try: @@ -1907,31 +1988,68 @@ def _get_transitions(tz): arr[0] = NPY_NAT + 1 except Exception: pass + elif _treat_tz_as_dateutil(tz): + if len(tz._trans_list): + # get utc trans times + trans_list = _get_utc_trans_times_from_dateutil_tz(tz) + arr = np.hstack([np.array([0], dtype='M8[s]'), # place holder for first item + np.array(trans_list, dtype='M8[s]')]).astype('M8[ns]') # all trans listed + arr = arr.view('i8') + # scale transitions correctly in numpy 1.6 + if _np_version_under1p7: + arr *= 1000000000 + arr[0] = NPY_NAT + 1 + elif _is_fixed_offset(tz): + arr = np.array([NPY_NAT + 1], dtype=np.int64) + else: + arr = np.array([], dtype='M8[ns]') else: arr = np.array([NPY_NAT + 1], dtype=np.int64) - trans_cache[tz] = arr - return trans_cache[tz] + trans_cache[cache_key] = arr + return trans_cache[cache_key] + + +cdef object _get_utc_trans_times_from_dateutil_tz(object tz): + ''' + Transition times in dateutil timezones are stored in local non-dst time. This code + converts them to UTC. It's the reverse of the code in dateutil.tz.tzfile.__init__. + ''' + new_trans = list(tz._trans_list) + last_std_offset = 0 + for i, (trans, tti) in enumerate(zip(tz._trans_list, tz._trans_idx)): + if not tti.isdst: + last_std_offset = tti.offset + new_trans[i] = trans - last_std_offset + return new_trans -def _get_deltas(tz): + +cdef object _get_deltas(object tz): """ Get UTC offsets in microseconds corresponding to DST transitions """ - try: - # tzoffset not hashable in Python 3 - hash(tz) - except TypeError: + cache_key = _tz_cache_key(tz) + if cache_key is None: num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 return np.array([num], dtype=np.int64) - if tz not in utc_offset_cache: - if hasattr(tz, '_utc_transition_times'): - utc_offset_cache[tz] = _unbox_utcoffsets(tz._transition_info) + if cache_key not in utc_offset_cache: + if _treat_tz_as_pytz(tz): + utc_offset_cache[cache_key] = _unbox_utcoffsets(tz._transition_info) + elif _treat_tz_as_dateutil(tz): + if len(tz._trans_list): + arr = np.array([v.offset for v in (tz._ttinfo_before,) + tz._trans_idx], dtype='i8') # + (tz._ttinfo_std,) + arr *= 1000000000 + utc_offset_cache[cache_key] = arr + elif _is_fixed_offset(tz): + utc_offset_cache[cache_key] = np.array([tz._ttinfo_std.offset], dtype='i8') * 1000000000 + else: + utc_offset_cache[cache_key] = np.array([], dtype='i8') else: # static tzinfo num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 - utc_offset_cache[tz] = np.array([num], dtype=np.int64) + utc_offset_cache[cache_key] = np.array([num], dtype=np.int64) - return utc_offset_cache[tz] + return utc_offset_cache[cache_key] cdef double total_seconds(object td): # Python 2.6 compat return ((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) // @@ -2019,7 +2137,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, bint infer_dst=False): # right side idx_shifted = _ensure_int64( np.maximum(0, trans.searchsorted(vals + DAY_NS, side='right') - 1)) - + for i in range(n): v = vals[i] - deltas[idx_shifted[i]] pos = bisect_right_i8(tdata, v, ntrans) - 1 @@ -2028,7 +2146,6 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, bint infer_dst=False): if v + deltas[pos] == vals[i]: result_b[i] = v - if infer_dst: dst_hours = np.empty(n, dtype=np.int64) dst_hours.fill(NPY_NAT) @@ -2569,8 +2686,7 @@ def date_normalize(ndarray[int64_t] stamps, tz=None): if tz is not None: tso = _TSObject() - if isinstance(tz, basestring): - tz = pytz.timezone(tz) + tz = maybe_get_tz(tz) result = _normalize_local(stamps, tz) else: for i in range(n): @@ -3173,8 +3289,7 @@ cpdef resolution(ndarray[int64_t] stamps, tz=None): int reso = D_RESO, curr_reso if tz is not None: - if isinstance(tz, basestring): - tz = pytz.timezone(tz) + tz = maybe_get_tz(tz) return _reso_local(stamps, tz) else: for i in range(n):