diff --git a/doc/source/v0.15.0.txt b/doc/source/v0.15.0.txt index c2d234b5a06c1..7ef8e1fac08d1 100644 --- a/doc/source/v0.15.0.txt +++ b/doc/source/v0.15.0.txt @@ -256,6 +256,9 @@ Bug Fixes - Bug in repeated timeseries line and area plot may result in ``ValueError`` or incorrect kind (:issue:`7733`) +- Bug in ``offsets.apply``, ``rollforward`` and ``rollback`` may reset nanosecond (:issue:`7697`) +- Bug in ``offsets.apply``, ``rollforward`` and ``rollback`` may raise ``AttributeError`` if ``Timestamp`` has ``dateutil`` tzinfo (:issue:`7697`) + - Bug in ``is_superperiod`` and ``is_subperiod`` cannot handle higher frequencies than ``S`` (:issue:`7760`, :issue:`7772`, :issue:`7803`) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 8f77f88910a3c..d2c9acedcee94 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -22,13 +22,14 @@ 'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd', 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253', 'Week', 'WeekOfMonth', 'Easter', - 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano'] + 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano', + 'DateOffset'] # convert to/from datetime/timestamp to allow invalid Timestamp ranges to pass thru def as_timestamp(obj): + if isinstance(obj, Timestamp): + return obj try: - if isinstance(obj, Timestamp): - return obj return Timestamp(obj) except (OutOfBoundsDatetime): pass @@ -45,22 +46,46 @@ def apply_wraps(func): def wrapper(self, other): if other is tslib.NaT: return tslib.NaT - if type(other) == date: - other = datetime(other.year, other.month, other.day) - if isinstance(other, (np.datetime64, datetime)): + elif isinstance(other, (timedelta, Tick, DateOffset)): + # timedelta path + return func(self, other) + elif isinstance(other, (np.datetime64, datetime, date)): other = as_timestamp(other) tz = getattr(other, 'tzinfo', None) - result = func(self, other) + nano = getattr(other, 'nanosecond', 0) - if self.normalize: - result = tslib.normalize_date(result) + try: + result = func(self, other) + + if self.normalize: + # normalize_date returns normal datetime + result = tslib.normalize_date(result) + result = Timestamp(result) - if isinstance(other, Timestamp) and not isinstance(result, Timestamp): - result = as_timestamp(result) + # nanosecond may be deleted depending on offset process + if not self.normalize and nano != 0: + if not isinstance(self, Nano) and result.nanosecond != nano: + if result.tz is not None: + # convert to UTC + value = tslib.tz_convert_single(result.value, 'UTC', result.tz) + else: + value = result.value + result = Timestamp(value + nano) + + if tz is not None and result.tzinfo is None: + result = tslib._localize_pydatetime(result, tz) + + except OutOfBoundsDatetime: + result = func(self, as_datetime(other)) + + if self.normalize: + # normalize_date returns normal datetime + result = tslib.normalize_date(result) + + if tz is not None and result.tzinfo is None: + result = tslib._localize_pydatetime(result, tz) - if tz is not None and result.tzinfo is None: - result = result.tz_localize(tz) return result return wrapper @@ -144,7 +169,6 @@ def __init__(self, n=1, normalize=False, **kwds): @apply_wraps def apply(self, other): - other = as_datetime(other) if len(self.kwds) > 0: if self.n > 0: for i in range(self.n): @@ -152,9 +176,9 @@ def apply(self, other): else: for i in range(-self.n): other = other - self._offset - return as_timestamp(other) + return other else: - return as_timestamp(other + timedelta(self.n)) + return other + timedelta(self.n) def isAnchored(self): return (self.n == 1) @@ -270,16 +294,16 @@ def __rmul__(self, someInt): def __neg__(self): return self.__class__(-self.n, normalize=self.normalize, **self.kwds) - @apply_wraps def rollback(self, dt): """Roll provided date backward to next offset only if not on offset""" + dt = as_timestamp(dt) if not self.onOffset(dt): dt = dt - self.__class__(1, normalize=self.normalize, **self.kwds) return dt - @apply_wraps def rollforward(self, dt): """Roll provided date forward to next offset only if not on offset""" + dt = as_timestamp(dt) if not self.onOffset(dt): dt = dt + self.__class__(1, normalize=self.normalize, **self.kwds) return dt @@ -452,8 +476,7 @@ def apply(self, other): if self.offset: result = result + self.offset - - return as_timestamp(result) + return result elif isinstance(other, (timedelta, Tick)): return BDay(self.n, offset=self.offset + other, @@ -550,7 +573,6 @@ def apply(self, other): else: roll = 'backward' - # Distinguish input cases to enhance performance if isinstance(other, datetime): date_in = other np_dt = np.datetime64(date_in.date()) @@ -563,8 +585,7 @@ def apply(self, other): if self.offset: result = result + self.offset - - return as_timestamp(result) + return result elif isinstance(other, (timedelta, Tick)): return BDay(self.n, offset=self.offset + other, @@ -613,11 +634,11 @@ def apply(self, other): n = self.n _, days_in_month = tslib.monthrange(other.year, other.month) if other.day != days_in_month: - other = as_datetime(other) + relativedelta(months=-1, day=31) + other = other + relativedelta(months=-1, day=31) if n <= 0: n = n + 1 - other = as_datetime(other) + relativedelta(months=n, day=31) - return as_timestamp(other) + other = other + relativedelta(months=n, day=31) + return other def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -638,8 +659,7 @@ def apply(self, other): if other.day > 1 and n <= 0: # then roll forward if n<=0 n += 1 - other = as_datetime(other) + relativedelta(months=n, day=1) - return as_timestamp(other) + return other + relativedelta(months=n, day=1) def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -657,9 +677,7 @@ def isAnchored(self): @apply_wraps def apply(self, other): - n = self.n - wkday, days_in_month = tslib.monthrange(other.year, other.month) lastBDay = days_in_month - max(((wkday + days_in_month - 1) % 7) - 4, 0) @@ -668,11 +686,11 @@ def apply(self, other): n = n - 1 elif n <= 0 and other.day > lastBDay: n = n + 1 - other = as_datetime(other) + relativedelta(months=n, day=31) + other = other + relativedelta(months=n, day=31) if other.weekday() > 4: other = other - BDay() - return as_timestamp(other) + return other _prefix = 'BM' @@ -683,7 +701,6 @@ class BusinessMonthBegin(MonthOffset): @apply_wraps def apply(self, other): n = self.n - wkday, _ = tslib.monthrange(other.year, other.month) first = _get_firstbday(wkday) @@ -691,15 +708,15 @@ def apply(self, other): # as if rolled forward already n += 1 elif other.day < first and n > 0: - other = as_datetime(other) + timedelta(days=first - other.day) + other = other + timedelta(days=first - other.day) n -= 1 - other = as_datetime(other) + relativedelta(months=n) + other = other + relativedelta(months=n) wkday, _ = tslib.monthrange(other.year, other.month) first = _get_firstbday(wkday) result = datetime(other.year, other.month, first, other.hour, other.minute, other.second, other.microsecond) - return as_timestamp(result) + return result def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -746,30 +763,29 @@ def __init__(self, n=1, normalize=False, **kwds): self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri') - self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, **kwds) - self.m_offset = MonthEnd(normalize=normalize) + self.cbday = CustomBusinessDay(n=self.n, **kwds) + self.m_offset = MonthEnd() @apply_wraps def apply(self,other): n = self.n - dt_in = other # First move to month offset - cur_mend = self.m_offset.rollforward(dt_in) + cur_mend = self.m_offset.rollforward(other) # Find this custom month offset cur_cmend = self.cbday.rollback(cur_mend) - + # handle zero case. arbitrarily rollforward - if n == 0 and dt_in != cur_cmend: + if n == 0 and other != cur_cmend: n += 1 - if dt_in < cur_cmend and n >= 1: + if other < cur_cmend and n >= 1: n -= 1 - elif dt_in > cur_cmend and n <= -1: + elif other > cur_cmend and n <= -1: n += 1 new = cur_mend + n * MonthEnd() result = self.cbday.rollback(new) - return as_timestamp(result) + return result class CustomBusinessMonthBegin(BusinessMixin, MonthOffset): """ @@ -824,7 +840,7 @@ def apply(self,other): new = cur_mbegin + n * MonthBegin() result = self.cbday.rollforward(new) - return as_timestamp(result) + return result class Week(DateOffset): """ @@ -856,23 +872,22 @@ def isAnchored(self): def apply(self, other): base = other if self.weekday is None: - return as_timestamp(as_datetime(other) + self.n * self._inc) + return other + self.n * self._inc if self.n > 0: k = self.n otherDay = other.weekday() if otherDay != self.weekday: - other = as_datetime(other) + timedelta((self.weekday - otherDay) % 7) + other = other + timedelta((self.weekday - otherDay) % 7) k = k - 1 - other = as_datetime(other) + other = other for i in range(k): other = other + self._inc else: k = self.n otherDay = other.weekday() if otherDay != self.weekday: - other = as_datetime(other) + timedelta((self.weekday - otherDay) % 7) - other = as_datetime(other) + other = other + timedelta((self.weekday - otherDay) % 7) for i in range(-k): other = other - self._inc @@ -979,20 +994,14 @@ def apply(self, other): else: months = self.n + 1 - other = self.getOffsetOfMonth(as_datetime(other) + relativedelta(months=months, day=1)) + other = self.getOffsetOfMonth(other + relativedelta(months=months, day=1)) other = datetime(other.year, other.month, other.day, base.hour, base.minute, base.second, base.microsecond) - if getattr(other, 'tzinfo', None) is not None: - other = other.tzinfo.localize(other) return other def getOffsetOfMonth(self, dt): w = Week(weekday=self.weekday) - - d = datetime(dt.year, dt.month, 1) - if getattr(dt, 'tzinfo', None) is not None: - d = dt.tzinfo.localize(d) - + d = datetime(dt.year, dt.month, 1, tzinfo=dt.tzinfo) d = w.rollforward(d) for i in range(self.week): @@ -1003,9 +1012,7 @@ def getOffsetOfMonth(self, dt): def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False - d = datetime(dt.year, dt.month, dt.day) - if getattr(dt, 'tzinfo', None) is not None: - d = dt.tzinfo.localize(d) + d = datetime(dt.year, dt.month, dt.day, tzinfo=dt.tzinfo) return d == self.getOffsetOfMonth(dt) @property @@ -1072,18 +1079,14 @@ def apply(self, other): else: months = self.n + 1 - return self.getOffsetOfMonth(as_datetime(other) + relativedelta(months=months, day=1)) + return self.getOffsetOfMonth(other + relativedelta(months=months, day=1)) def getOffsetOfMonth(self, dt): m = MonthEnd() - d = datetime(dt.year, dt.month, 1, dt.hour, dt.minute, dt.second, dt.microsecond) - if getattr(dt, 'tzinfo', None) is not None: - d = dt.tzinfo.localize(d) - + d = datetime(dt.year, dt.month, 1, dt.hour, dt.minute, + dt.second, dt.microsecond, tzinfo=dt.tzinfo) eom = m.rollforward(d) - w = Week(weekday=self.weekday) - return w.rollback(eom) def onOffset(self, dt): @@ -1175,13 +1178,11 @@ def apply(self, other): elif n <= 0 and other.day > lastBDay and monthsToGo == 0: n = n + 1 - other = as_datetime(other) + relativedelta(months=monthsToGo + 3 * n, day=31) - if getattr(base, 'tzinfo', None) is not None: - other = base.tzinfo.localize(other) + other = other + relativedelta(months=monthsToGo + 3 * n, day=31) + other = tslib._localize_pydatetime(other, base.tzinfo) if other.weekday() > 4: other = other - BDay() - - return as_timestamp(other) + return other def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -1219,8 +1220,6 @@ class BQuarterBegin(QuarterOffset): @apply_wraps def apply(self, other): n = self.n - other = as_datetime(other) - wkday, _ = tslib.monthrange(other.year, other.month) first = _get_firstbday(wkday) @@ -1244,9 +1243,7 @@ def apply(self, other): result = datetime(other.year, other.month, first, other.hour, other.minute, other.second, other.microsecond) - if getattr(other, 'tzinfo', None) is not None: - result = other.tzinfo.localize(result) - return as_timestamp(result) + return result class QuarterEnd(QuarterOffset): @@ -1272,12 +1269,9 @@ def isAnchored(self): @apply_wraps def apply(self, other): n = self.n - base = other other = datetime(other.year, other.month, other.day, other.hour, other.minute, other.second, other.microsecond) - other = as_datetime(other) - wkday, days_in_month = tslib.monthrange(other.year, other.month) monthsToGo = 3 - ((other.month - self.startingMonth) % 3) @@ -1288,9 +1282,7 @@ def apply(self, other): n = n - 1 other = other + relativedelta(months=monthsToGo + 3 * n, day=31) - if getattr(base, 'tzinfo', None) is not None: - other = base.tzinfo.localize(other) - return as_timestamp(other) + return other def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -1311,8 +1303,6 @@ def isAnchored(self): @apply_wraps def apply(self, other): n = self.n - other = as_datetime(other) - wkday, days_in_month = tslib.monthrange(other.year, other.month) monthsSince = (other.month - self.startingMonth) % 3 @@ -1326,7 +1316,7 @@ def apply(self, other): n = n + 1 other = other + relativedelta(months=3 * n - monthsSince, day=1) - return as_timestamp(other) + return other class YearOffset(DateOffset): @@ -1361,8 +1351,6 @@ class BYearEnd(YearOffset): @apply_wraps def apply(self, other): n = self.n - other = as_datetime(other) - wkday, days_in_month = tslib.monthrange(other.year, self.month) lastBDay = (days_in_month - max(((wkday + days_in_month - 1) % 7) - 4, 0)) @@ -1387,7 +1375,7 @@ def apply(self, other): if result.weekday() > 4: result = result - BDay() - return as_timestamp(result) + return result class BYearBegin(YearOffset): @@ -1399,8 +1387,6 @@ class BYearBegin(YearOffset): @apply_wraps def apply(self, other): n = self.n - other = as_datetime(other) - wkday, days_in_month = tslib.monthrange(other.year, self.month) first = _get_firstbday(wkday) @@ -1420,8 +1406,8 @@ def apply(self, other): other = other + relativedelta(years=years) wkday, days_in_month = tslib.monthrange(other.year, self.month) first = _get_firstbday(wkday) - return as_timestamp(datetime(other.year, self.month, first, other.hour, - other.minute, other.second, other.microsecond)) + return datetime(other.year, self.month, first, other.hour, + other.minute, other.second, other.microsecond) class YearEnd(YearOffset): @@ -1473,8 +1459,7 @@ def _rollf(date): else: # n == 0, roll forward result = _rollf(result) - - return as_timestamp(result) + return result def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -1490,15 +1475,15 @@ class YearBegin(YearOffset): @apply_wraps def apply(self, other): - def _increment(date): - year = date.year + def _increment(date, n): + year = date.year + n - 1 if date.month >= self.month: year += 1 return datetime(year, self.month, 1, date.hour, date.minute, date.second, date.microsecond) - def _decrement(date): - year = date.year + def _decrement(date, n): + year = date.year + n + 1 if date.month < self.month or (date.month == self.month and date.day == 1): year -= 1 @@ -1507,24 +1492,19 @@ def _decrement(date): def _rollf(date): if (date.month != self.month) or date.day > 1: - date = _increment(date) + date = _increment(date, 1) return date n = self.n result = other if n > 0: - while n > 0: - result = _increment(result) - n -= 1 + result = _increment(result, n) elif n < 0: - while n < 0: - result = _decrement(result) - n += 1 + result = _decrement(result, n) else: # n == 0, roll forward result = _rollf(result) - - return as_timestamp(result) + return result def onOffset(self, dt): if self.normalize and not _is_normalized(dt): @@ -1624,10 +1604,9 @@ def apply(self, other): datetime(other.year, self.startingMonth, 1)) next_year = self.get_year_end( datetime(other.year + 1, self.startingMonth, 1)) - if getattr(other, 'tzinfo', None) is not None: - prev_year = other.tzinfo.localize(prev_year) - cur_year = other.tzinfo.localize(cur_year) - next_year = other.tzinfo.localize(next_year) + prev_year = tslib._localize_pydatetime(prev_year, other.tzinfo) + cur_year = tslib._localize_pydatetime(cur_year, other.tzinfo) + next_year = tslib._localize_pydatetime(next_year, other.tzinfo) if n > 0: if other == prev_year: @@ -1686,9 +1665,7 @@ def get_year_end(self, dt): return self._get_year_end_last(dt) def get_target_month_end(self, dt): - target_month = datetime(dt.year, self.startingMonth, 1) - if getattr(dt, 'tzinfo', None) is not None: - target_month = dt.tzinfo.localize(target_month) + target_month = datetime(dt.year, self.startingMonth, 1, tzinfo=dt.tzinfo) next_month_first_of = target_month + relativedelta(months=+1) return next_month_first_of + relativedelta(days=-1) @@ -1706,9 +1683,7 @@ def _get_year_end_nearest(self, dt): return backward def _get_year_end_last(self, dt): - current_year = datetime(dt.year, self.startingMonth, 1) - if getattr(dt, 'tzinfo', None) is not None: - current_year = dt.tzinfo.localize(current_year) + current_year = datetime(dt.year, self.startingMonth, 1, tzinfo=dt.tzinfo) return current_year + self._offset_lwom @property @@ -1822,8 +1797,6 @@ def isAnchored(self): @apply_wraps def apply(self, other): base = other - other = as_datetime(other) - n = self.n if n > 0: @@ -1926,8 +1899,7 @@ def __init__(self, n=1, **kwds): def apply(self, other): currentEaster = easter(other.year) currentEaster = datetime(currentEaster.year, currentEaster.month, currentEaster.day) - if getattr(other, 'tzinfo', None) is not None: - currentEaster = other.tzinfo.localize(currentEaster) + currentEaster = tslib._localize_pydatetime(currentEaster, other.tzinfo) # NOTE: easter returns a datetime.date so we have to convert to type of other if self.n >= 0: @@ -2021,19 +1993,9 @@ def nanos(self): def apply(self, other): # Timestamp can handle tz and nano sec, thus no need to use apply_wraps - if type(other) == date: - other = datetime(other.year, other.month, other.day) - elif isinstance(other, (np.datetime64, datetime)): - other = as_timestamp(other) - - if isinstance(other, datetime): - result = other + self.delta - if self.normalize: - # normalize_date returns normal datetime - result = tslib.normalize_date(result) - return as_timestamp(result) - - elif isinstance(other, timedelta): + if isinstance(other, (datetime, np.datetime64, date)): + return as_timestamp(other) + self + if isinstance(other, timedelta): return other + self.delta elif isinstance(other, type(self)): return type(self)(self.n + other.n) @@ -2067,16 +2029,7 @@ def _delta_to_tick(delta): else: # pragma: no cover return Nano(nanos) - -def _delta_to_nanoseconds(delta): - if isinstance(delta, np.timedelta64): - return delta.astype('timedelta64[ns]').item() - elif isinstance(delta, Tick): - delta = delta.delta - - return (delta.days * 24 * 60 * 60 * 1000000 - + delta.seconds * 1000000 - + delta.microseconds) * 1000 +_delta_to_nanoseconds = tslib._delta_to_nanoseconds class Day(Tick): diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py index 9febec68bd458..d99cfb254cc48 100644 --- a/pandas/tseries/tests/test_offsets.py +++ b/pandas/tseries/tests/test_offsets.py @@ -22,8 +22,8 @@ from pandas.tseries.tools import parse_time_string, _maybe_get_tz import pandas.tseries.offsets as offsets -from pandas.tslib import monthrange, OutOfBoundsDatetime, NaT -from pandas.lib import Timestamp +from pandas.tslib import NaT, Timestamp +import pandas.tslib as tslib from pandas.util.testing import assertRaisesRegexp import pandas.util.testing as tm from pandas.tseries.offsets import BusinessMonthEnd, CacheableOffset, \ @@ -39,7 +39,7 @@ def test_monthrange(): import calendar for y in range(2000, 2013): for m in range(1, 13): - assert monthrange(y, m) == calendar.monthrange(y, m) + assert tslib.monthrange(y, m) == calendar.monthrange(y, m) #### @@ -99,6 +99,9 @@ class Base(tm.TestCase): skip_np_u1p7 = [offsets.CustomBusinessDay, offsets.CDay, offsets.CustomBusinessMonthBegin, offsets.CustomBusinessMonthEnd, offsets.Nano] + timezones = [None, 'UTC', 'Asia/Tokyo', 'US/Eastern', + 'dateutil/Asia/Tokyo', 'dateutil/US/Pacific'] + @property def offset_types(self): if _np_version_under1p7: @@ -118,6 +121,8 @@ def _get_offset(self, klass, value=1, normalize=False): klass = klass(n=value, week=1, weekday=5, normalize=normalize) elif klass is Week: klass = klass(n=value, weekday=5, normalize=normalize) + elif klass is DateOffset: + klass = klass(days=value, normalize=normalize) else: try: klass = klass(value, normalize=normalize) @@ -138,7 +143,18 @@ def test_apply_out_of_range(self): result = Timestamp('20080101') + offset self.assertIsInstance(result, datetime) - except (OutOfBoundsDatetime): + self.assertIsNone(result.tzinfo) + + tm._skip_if_no_pytz() + tm._skip_if_no_dateutil() + # Check tz is preserved + for tz in self.timezones: + t = Timestamp('20080101', tz=tz) + result = t + offset + self.assertIsInstance(result, datetime) + self.assertEqual(t.tzinfo, result.tzinfo) + + except (tslib.OutOfBoundsDatetime): raise except (ValueError, KeyError) as e: raise nose.SkipTest("cannot create out_of_range offset: {0} {1}".format(str(self).split('.')[-1],e)) @@ -152,6 +168,7 @@ def setUp(self): # are applied to 2011/01/01 09:00 (Saturday) # used for .apply and .rollforward self.expecteds = {'Day': Timestamp('2011-01-02 09:00:00'), + 'DateOffset': Timestamp('2011-01-02 09:00:00'), 'BusinessDay': Timestamp('2011-01-03 09:00:00'), 'CustomBusinessDay': Timestamp('2011-01-03 09:00:00'), 'CustomBusinessMonthEnd': Timestamp('2011-01-31 09:00:00'), @@ -181,8 +198,6 @@ def setUp(self): 'Micro': Timestamp('2011-01-01 09:00:00.000001'), 'Nano': Timestamp(np.datetime64('2011-01-01T09:00:00.000000001Z'))} - self.timezones = ['UTC', 'Asia/Tokyo', 'US/Eastern'] - def test_return_type(self): for offset in self.offset_types: offset = self._get_offset(offset) @@ -204,37 +219,48 @@ def _check_offsetfunc_works(self, offset, funcname, dt, expected, func = getattr(offset_s, funcname) result = func(dt) - self.assert_(isinstance(result, Timestamp)) + self.assertTrue(isinstance(result, Timestamp)) self.assertEqual(result, expected) result = func(Timestamp(dt)) - self.assert_(isinstance(result, Timestamp)) + self.assertTrue(isinstance(result, Timestamp)) self.assertEqual(result, expected) + # test nano second is preserved + result = func(Timestamp(dt) + Nano(5)) + self.assertTrue(isinstance(result, Timestamp)) + if normalize is False: + self.assertEqual(result, expected + Nano(5)) + else: + self.assertEqual(result, expected) + if isinstance(dt, np.datetime64): # test tz when input is datetime or Timestamp return tm._skip_if_no_pytz() - import pytz + tm._skip_if_no_dateutil() + for tz in self.timezones: expected_localize = expected.tz_localize(tz) + tz_obj = _maybe_get_tz(tz) + dt_tz = tslib._localize_pydatetime(dt, tz_obj) - dt_tz = pytz.timezone(tz).localize(dt) result = func(dt_tz) - self.assert_(isinstance(result, Timestamp)) + self.assertTrue(isinstance(result, Timestamp)) self.assertEqual(result, expected_localize) result = func(Timestamp(dt, tz=tz)) - self.assert_(isinstance(result, Timestamp)) + self.assertTrue(isinstance(result, Timestamp)) self.assertEqual(result, expected_localize) - def _check_nanofunc_works(self, offset, funcname, dt, expected): - offset = self._get_offset(offset) - func = getattr(offset, funcname) - - t1 = Timestamp(dt) - self.assertEqual(func(t1), expected) + # test nano second is preserved + result = func(Timestamp(dt, tz=tz) + Nano(5)) + self.assertTrue(isinstance(result, Timestamp)) + if normalize is False: + self.assertEqual(result, expected_localize + Nano(5)) + else: + self.assertEqual(result, expected_localize) def test_apply(self): sdt = datetime(2011, 1, 1, 9, 0) @@ -243,21 +269,18 @@ def test_apply(self): for offset in self.offset_types: for dt in [sdt, ndt]: expected = self.expecteds[offset.__name__] - if offset == Nano: - self._check_nanofunc_works(offset, 'apply', dt, expected) - else: - self._check_offsetfunc_works(offset, 'apply', dt, expected) + self._check_offsetfunc_works(offset, 'apply', dt, expected) - expected = Timestamp(expected.date()) - self._check_offsetfunc_works(offset, 'apply', dt, expected, - normalize=True) + expected = Timestamp(expected.date()) + self._check_offsetfunc_works(offset, 'apply', dt, expected, + normalize=True) def test_rollforward(self): expecteds = self.expecteds.copy() # result will not be changed if the target is on the offset no_changes = ['Day', 'MonthBegin', 'YearBegin', 'Week', 'Hour', 'Minute', - 'Second', 'Milli', 'Micro', 'Nano'] + 'Second', 'Milli', 'Micro', 'Nano', 'DateOffset'] for n in no_changes: expecteds[n] = Timestamp('2011/01/01 09:00') @@ -267,6 +290,7 @@ def test_rollforward(self): norm_expected[k] = Timestamp(norm_expected[k].date()) normalized = {'Day': Timestamp('2011-01-02 00:00:00'), + 'DateOffset': Timestamp('2011-01-02 00:00:00'), 'MonthBegin': Timestamp('2011-02-01 00:00:00'), 'YearBegin': Timestamp('2012-01-01 00:00:00'), 'Week': Timestamp('2011-01-08 00:00:00'), @@ -283,13 +307,10 @@ def test_rollforward(self): for offset in self.offset_types: for dt in [sdt, ndt]: expected = expecteds[offset.__name__] - if offset == Nano: - self._check_nanofunc_works(offset, 'rollforward', dt, expected) - else: - self._check_offsetfunc_works(offset, 'rollforward', dt, expected) - expected = norm_expected[offset.__name__] - self._check_offsetfunc_works(offset, 'rollforward', dt, expected, - normalize=True) + self._check_offsetfunc_works(offset, 'rollforward', dt, expected) + expected = norm_expected[offset.__name__] + self._check_offsetfunc_works(offset, 'rollforward', dt, expected, + normalize=True) def test_rollback(self): expecteds = {'BusinessDay': Timestamp('2010-12-31 09:00:00'), @@ -314,7 +335,7 @@ def test_rollback(self): # result will not be changed if the target is on the offset for n in ['Day', 'MonthBegin', 'YearBegin', 'Week', 'Hour', 'Minute', - 'Second', 'Milli', 'Micro', 'Nano']: + 'Second', 'Milli', 'Micro', 'Nano', 'DateOffset']: expecteds[n] = Timestamp('2011/01/01 09:00') # but be changed when normalize=True @@ -323,6 +344,7 @@ def test_rollback(self): norm_expected[k] = Timestamp(norm_expected[k].date()) normalized = {'Day': Timestamp('2010-12-31 00:00:00'), + 'DateOffset': Timestamp('2010-12-31 00:00:00'), 'MonthBegin': Timestamp('2010-12-01 00:00:00'), 'YearBegin': Timestamp('2010-01-01 00:00:00'), 'Week': Timestamp('2010-12-25 00:00:00'), @@ -339,27 +361,24 @@ def test_rollback(self): for offset in self.offset_types: for dt in [sdt, ndt]: expected = expecteds[offset.__name__] - if offset == Nano: - self._check_nanofunc_works(offset, 'rollback', dt, expected) - else: - self._check_offsetfunc_works(offset, 'rollback', dt, expected) + self._check_offsetfunc_works(offset, 'rollback', dt, expected) - expected = norm_expected[offset.__name__] - self._check_offsetfunc_works(offset, 'rollback', - dt, expected, normalize=True) + expected = norm_expected[offset.__name__] + self._check_offsetfunc_works(offset, 'rollback', + dt, expected, normalize=True) def test_onOffset(self): for offset in self.offset_types: dt = self.expecteds[offset.__name__] offset_s = self._get_offset(offset) - self.assert_(offset_s.onOffset(dt)) + self.assertTrue(offset_s.onOffset(dt)) # when normalize=True, onOffset checks time is 00:00:00 offset_n = self._get_offset(offset, normalize=True) - self.assert_(not offset_n.onOffset(dt)) + self.assertFalse(offset_n.onOffset(dt)) date = datetime(dt.year, dt.month, dt.day) - self.assert_(offset_n.onOffset(date)) + self.assertTrue(offset_n.onOffset(date)) def test_add(self): dt = datetime(2011, 1, 1, 9, 0) @@ -2482,6 +2501,13 @@ def test_offset(self): datetime(2005, 12, 30): datetime(2006, 1, 1), datetime(2005, 12, 31): datetime(2006, 1, 1), })) + tests.append((YearBegin(3), + {datetime(2008, 1, 1): datetime(2011, 1, 1), + datetime(2008, 6, 30): datetime(2011, 1, 1), + datetime(2008, 12, 31): datetime(2011, 1, 1), + datetime(2005, 12, 30): datetime(2008, 1, 1), + datetime(2005, 12, 31): datetime(2008, 1, 1), })) + tests.append((YearBegin(-1), {datetime(2007, 1, 1): datetime(2006, 1, 1), datetime(2007, 1, 15): datetime(2007, 1, 1), @@ -2509,12 +2535,25 @@ def test_offset(self): datetime(2007, 12, 15): datetime(2008, 4, 1), datetime(2012, 1, 31): datetime(2012, 4, 1), })) + tests.append((YearBegin(4, month=4), + {datetime(2007, 4, 1): datetime(2011, 4, 1), + datetime(2007, 4, 15): datetime(2011, 4, 1), + datetime(2007, 3, 1): datetime(2010, 4, 1), + datetime(2007, 12, 15): datetime(2011, 4, 1), + datetime(2012, 1, 31): datetime(2015, 4, 1), })) + tests.append((YearBegin(-1, month=4), {datetime(2007, 4, 1): datetime(2006, 4, 1), datetime(2007, 3, 1): datetime(2006, 4, 1), datetime(2007, 12, 15): datetime(2007, 4, 1), datetime(2012, 1, 31): datetime(2011, 4, 1), })) + tests.append((YearBegin(-3, month=4), + {datetime(2007, 4, 1): datetime(2004, 4, 1), + datetime(2007, 3, 1): datetime(2004, 4, 1), + datetime(2007, 12, 15): datetime(2005, 4, 1), + datetime(2012, 1, 31): datetime(2009, 4, 1), })) + for offset, cases in tests: for base, expected in compat.iteritems(cases): assertEq(offset, base, expected) diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index c06d8a3ba9a05..655b92cfe70f3 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -1051,6 +1051,26 @@ cdef inline void _localize_tso(_TSObject obj, object tz): obj.tzinfo = tz +def _localize_pydatetime(object dt, object tz): + ''' + Take a datetime/Timestamp in UTC and localizes to timezone tz. + ''' + if tz is None: + return dt + elif isinstance(dt, Timestamp): + return dt.tz_localize(tz) + elif tz == 'UTC' or tz is UTC: + return UTC.localize(dt) + + elif _treat_tz_as_pytz(tz): + # datetime.replace may return incorrect result in pytz + return tz.localize(dt) + elif _treat_tz_as_dateutil(tz): + return dt.replace(tzinfo=tz) + else: + raise ValueError(type(tz), tz) + + def get_timezone(tz): return _get_zone(tz)