Repository: pandas-dev/pandas

Issue:
date_range bug?
If 'start' is a timezone-aware datetime.datetime or Timestamp, then it will get shifted by several hours.

``` python
In [1]: from pandas import *

In [2]: start = Timestamp('20130220 10:00', tz='US/Eastern')

In [3]: start
Out[3]: <Timestamp: 2013-02-20 10:00:00-0500 EST, tz=US/Eastern>

In [4]: dr = date_range(start, periods=2)

In [5]: dr
Out[5]: 
<class 'pandas.tseries.index.DatetimeIndex'>
[2013-02-20 15:00:00, 2013-02-21 15:00:00]
Length: 2, Freq: D, Timezone: US/Eastern

In [6]: dr[0]
Out[6]: <Timestamp: 2013-02-20 15:00:00-0500 EST, tz=US/Eastern>
```

It seems that start' time component was treated as if it's UTC by date_range.



Discussion:
date_range permits to include the timezone with the "tz" arguments like this: `dr = date_range(start, periods=2, tz='US/Eastern')`. However this fails with an assert that the inferred timezone is not the same as the given timezone. So I suppose the inferred timezone is wrong. 
Edit: this seems like a deeper problem unrelated to this issue. I opened a [new issue for that](https://github.com/pydata/pandas/issues/2926).


Resolving patch:
diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py
--- a/pandas/tseries/index.py
+++ b/pandas/tseries/index.py
@@ -298,7 +298,11 @@ def _generate(cls, start, end, periods, name, offset,
         if end is not None:
             end = Timestamp(end)
 
-        inferred_tz = tools._infer_tzinfo(start, end)
+        try:
+            inferred_tz = tools._infer_tzinfo(start, end)
+        except:
+            raise ValueError('Start and end cannot both be tz-aware with '
+                             'different timezones')
 
         if tz is not None and inferred_tz is not None:
             assert(inferred_tz == tz)
@@ -1538,17 +1542,21 @@ def _generate_regular_range(start, end, periods, offset):
             b = Timestamp(start).value
             e = Timestamp(end).value
             e += stride - e % stride
+            # end.tz == start.tz by this point due to _generate implementation
+            tz = start.tz
         elif start is not None:
             b = Timestamp(start).value
             e = b + periods * stride
+            tz = start.tz
         elif end is not None:
             e = Timestamp(end).value + stride
             b = e - periods * stride
+            tz = end.tz
         else:
             raise NotImplementedError
 
         data = np.arange(b, e, stride, dtype=np.int64)
-        data = data.view(_NS_DTYPE)
+        data = DatetimeIndex._simple_new(data, None, tz=tz)
     else:
         if isinstance(start, Timestamp):
             start = start.to_pydatetime()
@@ -1723,7 +1731,8 @@ def _process_concat_data(to_concat, name):
             else:
                 to_concat = [x.values for x in to_concat]
 
-            klass = DatetimeIndex
+            # well, technically not a "class" anymore...oh well
+            klass = DatetimeIndex._simple_new
             kwargs = {'tz': tz}
             concat = com._concat_compat
     else:


Test patch:
diff --git a/pandas/tseries/tests/test_daterange.py b/pandas/tseries/tests/test_daterange.py
--- a/pandas/tseries/tests/test_daterange.py
+++ b/pandas/tseries/tests/test_daterange.py
@@ -15,10 +15,18 @@
 import pandas.core.datetools as datetools
 
 
+def _skip_if_no_pytz():
+    try:
+        import pytz
+    except ImportError:
+        raise nose.SkipTest
+
+
 def eq_gen_range(kwargs, expected):
     rng = generate_range(**kwargs)
     assert(np.array_equal(list(rng), expected))
 
+
 START, END = datetime(2009, 1, 1), datetime(2010, 1, 1)
 
 
@@ -246,11 +254,11 @@ def test_intersection_bug(self):
     def test_summary(self):
         self.rng.summary()
         self.rng[2:2].summary()
-        try:
-            import pytz
-            bdate_range('1/1/2005', '1/1/2009', tz=pytz.utc).summary()
-        except Exception:
-            pass
+
+    def test_summary_pytz(self):
+        _skip_if_no_pytz()
+        import pytz
+        bdate_range('1/1/2005', '1/1/2009', tz=pytz.utc).summary()
 
     def test_misc(self):
         end = datetime(2009, 5, 13)
@@ -298,6 +306,29 @@ def test_range_bug(self):
         exp_values = [start + i * offset for i in range(5)]
         self.assert_(np.array_equal(result, DatetimeIndex(exp_values)))
 
+    def test_range_tz(self):
+        # GH 2906
+        _skip_if_no_pytz()
+        from pytz import timezone 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)
+
 
 if __name__ == '__main__':
     import nose
diff --git a/pandas/tseries/tests/test_timeseries.py b/pandas/tseries/tests/test_timeseries.py
--- a/pandas/tseries/tests/test_timeseries.py
+++ b/pandas/tseries/tests/test_timeseries.py
@@ -42,6 +42,13 @@
 from numpy.testing.decorators import slow
 
 
+def _skip_if_no_pytz():
+    try:
+        import pytz
+    except ImportError:
+        raise nose.SkipTest
+
+
 class TestTimeSeriesDuplicates(unittest.TestCase):
     _multiprocess_can_split_ = True
 
@@ -168,13 +175,6 @@ def assert_range_equal(left, right):
     assert(left.tz == right.tz)
 
 
-def _skip_if_no_pytz():
-    try:
-        import pytz
-    except ImportError:
-        raise nose.SkipTest
-
-
 class TestTimeSeries(unittest.TestCase):
     _multiprocess_can_split_ = True
 
@@ -1265,6 +1265,29 @@ def test_append_concat(self):
         self.assert_(rng1.append(rng1).name == 'foo')
         self.assert_(rng1.append(rng2).name is None)
 
+    def test_append_concat_tz(self):
+        #GH 2938
+        _skip_if_no_pytz()
+
+        rng = date_range('5/8/2012 1:45', periods=10, freq='5T',
+                         tz='US/Eastern')
+        rng2 = date_range('5/8/2012 2:35', periods=10, freq='5T',
+                         tz='US/Eastern')
+        rng3 = date_range('5/8/2012 1:45', periods=20, freq='5T',
+                         tz='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.assert_(x[0].dtype == np.dtype('M8[ns]'))