From 728b0a7451f9d3a93559280724a9bf348cbfd21d Mon Sep 17 00:00:00 2001 From: sinhrks Date: Sun, 18 May 2014 16:31:26 +0900 Subject: [PATCH] BUG: Offset.apply dont preserve time --- doc/source/timeseries.rst | 19 ++ doc/source/v0.14.1.txt | 10 ++ pandas/tseries/offsets.py | 256 ++++++++++++++++----------- pandas/tseries/tests/test_offsets.py | 219 +++++++++++++++++++++-- 4 files changed, 384 insertions(+), 120 deletions(-) diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index 0303b41e42e55..795bbca673f77 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -539,6 +539,25 @@ The ``rollforward`` and ``rollback`` methods do exactly what you would expect: It's definitely worth exploring the ``pandas.tseries.offsets`` module and the various docstrings for the classes. +These operations (``apply``, ``rollforward`` and ``rollback``) preserves time (hour, minute, etc) information by default. To reset time, use ``normalize=True`` keyword when create offset instance. If ``normalize=True``, result is normalized after the function is applied. + + + .. ipython:: python + + day = Day() + day.apply(Timestamp('2014-01-01 09:00')) + + day = Day(normalize=True) + day.apply(Timestamp('2014-01-01 09:00')) + + hour = Hour() + hour.apply(Timestamp('2014-01-01 22:00')) + + hour = Hour(normalize=True) + hour.apply(Timestamp('2014-01-01 22:00')) + hour.apply(Timestamp('2014-01-01 23:00')) + + Parametric offsets ~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/v0.14.1.txt b/doc/source/v0.14.1.txt index 2b76da1434ba3..01407856dfe18 100644 --- a/doc/source/v0.14.1.txt +++ b/doc/source/v0.14.1.txt @@ -33,8 +33,18 @@ API changes +- All ``offsets`` suppports ``normalize`` keyword to specify whether ``offsets.apply``, ``rollforward`` and ``rollback`` resets time (hour, minute, etc) or not (default ``False``, preserves time) (:issue:`7156`) + .. ipython:: python + + import pandas.tseries.offsets as offsets + + day = offsets.Day() + day.apply(Timestamp('2014-01-01 09:00')) + + day = offsets.Day(normalize=True) + day.apply(Timestamp('2014-01-01 09:00')) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 03ee51470763d..9cbef50f2d82f 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -38,15 +38,31 @@ def as_datetime(obj): obj = f() return obj -def apply_nat(func): +def apply_wraps(func): @functools.wraps(func) def wrapper(self, other): if other is tslib.NaT: return tslib.NaT - else: - return func(self, other) + if type(other) == date: + other = datetime(other.year, other.month, other.day) + + result = func(self, other) + + if self.normalize: + result = tslib.normalize_date(result) + + if isinstance(other, Timestamp) and not isinstance(result, Timestamp): + result = as_timestamp(result) + return result return wrapper + +def _is_normalized(dt): + if (dt.hour != 0 or dt.minute != 0 or dt.second != 0 + or dt.microsecond != 0 or getattr(dt, 'nanosecond', 0) != 0): + return False + return True + #---------------------------------------------------------------------- # DateOffset @@ -106,15 +122,16 @@ def __add__(date): _cacheable = False _normalize_cache = True - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = int(n) + self.normalize = normalize self.kwds = kwds if len(kwds) > 0: self._offset = relativedelta(**kwds) else: self._offset = timedelta(1) - @apply_nat + @apply_wraps def apply(self, other): other = as_datetime(other) if len(self.kwds) > 0: @@ -151,7 +168,7 @@ def __repr__(self): if hasattr(self, '_named'): return self._named className = getattr(self, '_outputName', type(self).__name__) - exclude = set(['n', 'inc']) + exclude = set(['n', 'inc', 'normalize']) attrs = [] for attr in sorted(self.__dict__): if ((attr == 'kwds' and len(self.kwds) == 0) @@ -242,25 +259,23 @@ def __rmul__(self, someInt): def __neg__(self): return self.__class__(-self.n, **self.kwds) + @apply_wraps def rollback(self, dt): """Roll provided date backward to next offset only if not on offset""" - if type(dt) == date: - dt = datetime(dt.year, dt.month, dt.day) - if not self.onOffset(dt): dt = dt - self.__class__(1, **self.kwds) return dt + @apply_wraps def rollforward(self, dt): """Roll provided date forward to next offset only if not on offset""" - if type(dt) == date: - dt = datetime(dt.year, dt.month, dt.day) - if not self.onOffset(dt): dt = dt + self.__class__(1, **self.kwds) return dt def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False # XXX, see #1395 if type(self) == DateOffset or isinstance(self, Tick): return True @@ -341,11 +356,11 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset): """ _prefix = 'B' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = int(n) + self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - self.normalize = kwds.get('normalize', False) @property def freqstr(self): @@ -398,7 +413,7 @@ def get_str(td): def isAnchored(self): return (self.n == 1) - @apply_nat + @apply_wraps def apply(self, other): if isinstance(other, datetime): n = self.n @@ -424,9 +439,6 @@ def apply(self, other): if result.weekday() < 5: n -= k - if self.normalize: - result = datetime(result.year, result.month, result.day) - if self.offset: result = result + self.offset @@ -439,8 +451,9 @@ def apply(self, other): raise ApplyTypeError('Only know how to combine business day with ' 'datetime or timedelta.') - @classmethod - def onOffset(cls, dt): + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False return dt.weekday() < 5 @@ -472,11 +485,11 @@ class CustomBusinessDay(BusinessDay): _cacheable = False _prefix = 'C' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = int(n) + self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - self.normalize = kwds.get('normalize', False) self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri') if 'calendar' in kwds: @@ -519,7 +532,7 @@ def __setstate__(self, state): self.__dict__ = state self._set_busdaycalendar() - @apply_nat + @apply_wraps def apply(self, other): if self.n <= 0: roll = 'forward' @@ -535,10 +548,7 @@ def apply(self, other): busdaycal=self.busdaycalendar) dt_date = np_incr_dt.astype(datetime) - if not self.normalize: - result = datetime.combine(dt_date,date_in.time()) - else: - result = dt_date + result = datetime.combine(dt_date, date_in.time()) if self.offset: result = result + self.offset @@ -552,11 +562,7 @@ def apply(self, other): np_incr_dt = np.busday_offset(np_day, self.n, roll=roll, busdaycal=self.busdaycalendar) - - if not self.normalize: - result = np_incr_dt + np_time - else: - result = np_incr_dt + result = np_incr_dt + np_time if self.offset: result = result + self.offset @@ -582,6 +588,8 @@ def _to_dt64(dt, dtype='datetime64'): return dt def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False day64 = self._to_dt64(dt,'datetime64[D]') return np.is_busday(day64, busdaycal=self.busdaycalendar) @@ -598,11 +606,8 @@ def name(self): class MonthEnd(MonthOffset): """DateOffset of one month end""" - @apply_nat + @apply_wraps def apply(self, other): - other = datetime(other.year, other.month, other.day, - tzinfo=other.tzinfo) - n = self.n _, days_in_month = tslib.monthrange(other.year, other.month) if other.day != days_in_month: @@ -612,8 +617,9 @@ def apply(self, other): other = as_datetime(other) + relativedelta(months=n, day=31) return as_timestamp(other) - @classmethod - def onOffset(cls, dt): + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False days_in_month = tslib.monthrange(dt.year, dt.month)[1] return dt.day == days_in_month @@ -623,7 +629,7 @@ def onOffset(cls, dt): class MonthBegin(MonthOffset): """DateOffset of one month at beginning""" - @apply_nat + @apply_wraps def apply(self, other): n = self.n @@ -633,8 +639,9 @@ def apply(self, other): other = as_datetime(other) + relativedelta(months=n, day=1) return as_timestamp(other) - @classmethod - def onOffset(cls, dt): + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False return dt.day == 1 _prefix = 'MS' @@ -646,9 +653,8 @@ class BusinessMonthEnd(MonthOffset): def isAnchored(self): return (self.n == 1) - @apply_nat + @apply_wraps def apply(self, other): - other = datetime(other.year, other.month, other.day) n = self.n @@ -672,7 +678,7 @@ def apply(self, other): class BusinessMonthBegin(MonthOffset): """DateOffset of one business month at beginning""" - @apply_nat + @apply_wraps def apply(self, other): n = self.n @@ -689,11 +695,13 @@ def apply(self, other): other = as_datetime(other) + relativedelta(months=n) wkday, _ = tslib.monthrange(other.year, other.month) first = _get_firstbday(wkday) - result = datetime(other.year, other.month, first) + result = datetime(other.year, other.month, first, other.hour, other.minute, + other.second, other.microsecond) return as_timestamp(result) - @classmethod - def onOffset(cls, dt): + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False first_weekday, _ = tslib.monthrange(dt.year, dt.month) if first_weekday == 5: return dt.day == 3 @@ -730,16 +738,16 @@ class CustomBusinessMonthEnd(BusinessMixin, MonthOffset): _cacheable = False _prefix = 'CBM' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = int(n) + self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - self.normalize = kwds.get('normalize', False) self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri') - self.cbday = CustomBusinessDay(n=self.n,**kwds) - self.m_offset = MonthEnd() + self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, **kwds) + self.m_offset = MonthEnd(normalize=normalize) - @apply_nat + @apply_wraps def apply(self,other): n = self.n dt_in = other @@ -785,16 +793,16 @@ class CustomBusinessMonthBegin(BusinessMixin, MonthOffset): _cacheable = False _prefix = 'CBMS' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = int(n) + self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - self.normalize = kwds.get('normalize', False) self.weekmask = kwds.get('weekmask', 'Mon Tue Wed Thu Fri') - self.cbday = CustomBusinessDay(n=self.n,**kwds) - self.m_offset = MonthBegin() + self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, **kwds) + self.m_offset = MonthBegin(normalize=normalize) - @apply_nat + @apply_wraps def apply(self,other): n = self.n dt_in = other @@ -826,8 +834,9 @@ class Week(DateOffset): Always generate specific day of week. 0 for Monday """ - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.weekday = kwds.get('weekday', None) if self.weekday is not None: @@ -841,8 +850,9 @@ def __init__(self, n=1, **kwds): def isAnchored(self): return (self.n == 1 and self.weekday is not None) - @apply_nat + @apply_wraps def apply(self, other): + base = other if self.weekday is None: return as_timestamp(as_datetime(other) + self.n * self._inc) @@ -863,9 +873,14 @@ def apply(self, other): other = as_datetime(other) for i in range(-k): other = other - self._inc - return as_timestamp(other) + + other = datetime(other.year, other.month, other.day, + base.hour, base.minute, base.second, base.microsecond) + return other def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False return dt.weekday() == self.weekday _prefix = 'W' @@ -926,8 +941,9 @@ class WeekOfMonth(DateOffset): 6: Sundays """ - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.weekday = kwds['weekday'] self.week = kwds['week'] @@ -943,8 +959,9 @@ def __init__(self, n=1, **kwds): self.kwds = kwds - @apply_nat + @apply_wraps def apply(self, other): + base = other offsetOfMonth = self.getOffsetOfMonth(other) if offsetOfMonth > other: @@ -960,8 +977,10 @@ def apply(self, other): else: months = self.n + 1 - return self.getOffsetOfMonth(as_datetime(other) + relativedelta(months=months, - day=1)) + other = self.getOffsetOfMonth(as_datetime(other) + relativedelta(months=months, day=1)) + other = datetime(other.year, other.month, other.day, + base.hour, base.minute, base.second, base.microsecond) + return other def getOffsetOfMonth(self, dt): w = Week(weekday=self.weekday) @@ -975,7 +994,10 @@ def getOffsetOfMonth(self, dt): return d def onOffset(self, dt): - return dt == self.getOffsetOfMonth(dt) + if self.normalize and not _is_normalized(dt): + return False + d = datetime(dt.year, dt.month, dt.day) + return d == self.getOffsetOfMonth(dt) @property def rule_code(self): @@ -1010,8 +1032,9 @@ class LastWeekOfMonth(DateOffset): 5: Saturdays 6: Sundays """ - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.weekday = kwds['weekday'] if self.n == 0: @@ -1023,7 +1046,7 @@ def __init__(self, n=1, **kwds): self.kwds = kwds - @apply_nat + @apply_wraps def apply(self, other): offsetOfMonth = self.getOffsetOfMonth(other) @@ -1044,7 +1067,7 @@ def apply(self, other): def getOffsetOfMonth(self, dt): m = MonthEnd() - d = datetime(dt.year, dt.month, 1) + d = datetime(dt.year, dt.month, 1, dt.hour, dt.minute, dt.second, dt.microsecond) eom = m.rollforward(d) @@ -1053,6 +1076,8 @@ def getOffsetOfMonth(self, dt): return w.rollback(eom) def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False return dt == self.getOffsetOfMonth(dt) @property @@ -1080,8 +1105,9 @@ class QuarterOffset(DateOffset): # TODO: Consider combining QuarterOffset and YearOffset __init__ at some # point - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.startingMonth = kwds.get('startingMonth', self._default_startingMonth) @@ -1117,7 +1143,7 @@ class BQuarterEnd(QuarterOffset): _from_name_startingMonth = 12 _prefix = 'BQ' - @apply_nat + @apply_wraps def apply(self, other): n = self.n @@ -1142,6 +1168,8 @@ def apply(self, other): return as_timestamp(other) def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False modMonth = (dt.month - self.startingMonth) % 3 return BMonthEnd().onOffset(dt) and modMonth == 0 @@ -1172,7 +1200,7 @@ class BQuarterBegin(QuarterOffset): _from_name_startingMonth = 1 _prefix = 'BQS' - @apply_nat + @apply_wraps def apply(self, other): n = self.n other = as_datetime(other) @@ -1213,8 +1241,9 @@ class QuarterEnd(QuarterOffset): _default_startingMonth = 3 _prefix = 'Q' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.startingMonth = kwds.get('startingMonth', 3) self.kwds = kwds @@ -1222,7 +1251,7 @@ def __init__(self, n=1, **kwds): def isAnchored(self): return (self.n == 1 and self.startingMonth is not None) - @apply_nat + @apply_wraps def apply(self, other): n = self.n other = as_datetime(other) @@ -1241,6 +1270,8 @@ def apply(self, other): return as_timestamp(other) def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False modMonth = (dt.month - self.startingMonth) % 3 return MonthEnd().onOffset(dt) and modMonth == 0 @@ -1254,7 +1285,7 @@ class QuarterBegin(QuarterOffset): def isAnchored(self): return (self.n == 1 and self.startingMonth is not None) - @apply_nat + @apply_wraps def apply(self, other): n = self.n other = as_datetime(other) @@ -1278,13 +1309,13 @@ def apply(self, other): class YearOffset(DateOffset): """DateOffset that just needs a month""" - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.month = kwds.get('month', self._default_month) if self.month < 1 or self.month > 12: raise ValueError('Month must go from 1 to 12') - DateOffset.__init__(self, n=n, **kwds) + DateOffset.__init__(self, n=n, normalize=normalize, **kwds) @classmethod def _from_name(cls, suffix=None): @@ -1304,7 +1335,7 @@ class BYearEnd(YearOffset): _default_month = 12 _prefix = 'BA' - @apply_nat + @apply_wraps def apply(self, other): n = self.n other = as_datetime(other) @@ -1342,7 +1373,7 @@ class BYearBegin(YearOffset): _default_month = 1 _prefix = 'BAS' - @apply_nat + @apply_wraps def apply(self, other): n = self.n other = as_datetime(other) @@ -1366,7 +1397,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)) + return as_timestamp(datetime(other.year, self.month, first, other.hour, + other.minute, other.second, other.microsecond)) class YearEnd(YearOffset): @@ -1374,7 +1406,7 @@ class YearEnd(YearOffset): _default_month = 12 _prefix = 'A' - @apply_nat + @apply_wraps def apply(self, other): def _increment(date): if date.month == self.month: @@ -1422,6 +1454,8 @@ def _rollf(date): return as_timestamp(result) def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False wkday, days_in_month = tslib.monthrange(dt.year, self.month) return self.month == dt.month and dt.day == days_in_month @@ -1431,7 +1465,7 @@ class YearBegin(YearOffset): _default_month = 1 _prefix = 'AS' - @apply_nat + @apply_wraps def apply(self, other): def _increment(date): year = date.year @@ -1470,6 +1504,8 @@ def _rollf(date): return as_timestamp(result) def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False return dt.month == self.month and dt.day == 1 @@ -1515,8 +1551,9 @@ class FY5253(DateOffset): _suffix_prefix_last = 'L' _suffix_prefix_nearest = 'N' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.startingMonth = kwds['startingMonth'] self.weekday = kwds["weekday"] @@ -1543,6 +1580,9 @@ def isAnchored(self): and self.weekday is not None def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False + dt = datetime(dt.year, dt.month, dt.day) year_end = self.get_year_end(dt) if self.variation == "nearest": @@ -1552,7 +1592,7 @@ def onOffset(self, dt): else: return year_end == dt - @apply_nat + @apply_wraps def apply(self, other): n = self.n prev_year = self.get_year_end( @@ -1581,7 +1621,11 @@ def apply(self, other): else: assert False - return self.get_year_end(datetime(year + n, self.startingMonth, 1)) + result = self.get_year_end(datetime(year + n, self.startingMonth, 1)) + + result = datetime(result.year, result.month, result.day, + other.hour, other.minute, other.second, other.microsecond) + return result else: n = -n if other == prev_year: @@ -1602,7 +1646,11 @@ def apply(self, other): else: assert False - return self.get_year_end(datetime(year - n, self.startingMonth, 1)) + result = self.get_year_end(datetime(year - n, self.startingMonth, 1)) + + result = datetime(result.year, result.month, result.day, + other.hour, other.minute, other.second, other.microsecond) + return result def get_year_end(self, dt): if self.variation == "nearest": @@ -1721,8 +1769,9 @@ class FY5253Quarter(DateOffset): _prefix = 'REQ' - def __init__(self, n=1, **kwds): + def __init__(self, n=1, normalize=False, **kwds): self.n = n + self.normalize = normalize self.qtr_with_extra_week = kwds["qtr_with_extra_week"] @@ -1739,9 +1788,11 @@ def __init__(self, n=1, **kwds): def isAnchored(self): return self.n == 1 and self._offset.isAnchored() - @apply_nat + @apply_wraps def apply(self, other): + base = other other = as_datetime(other) + n = self.n if n > 0: @@ -1776,7 +1827,8 @@ def apply(self, other): other = end n -= 1 break - + other = datetime(other.year, other.month, other.day, + base.hour, base.minute, base.second, base.microsecond) return other def get_weeks(self, dt): @@ -1802,6 +1854,8 @@ def year_has_extra_week(self, dt): return week_in_year == 53 def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False if self._offset.onOffset(dt): return True @@ -1837,7 +1891,7 @@ class Easter(DateOffset): def __init__(self, n=1, **kwds): super(Easter, self).__init__(n, **kwds) - @apply_nat + @apply_wraps def apply(self, other): currentEaster = easter(other.year) @@ -1854,17 +1908,14 @@ def apply(self, other): new = easter(other.year + self.n + 1) else: new = easter(other.year + self.n) - - # FIXME: There has to be a better way to do this, but I don't know what it is - if isinstance(other, Timestamp): - return as_timestamp(new) - elif isinstance(other, datetime): - return datetime(new.year, new.month, new.day) - else: - return new - - @classmethod - def onOffset(cls, dt): + + new = datetime(new.year, new.month, new.day, other.hour, + other.minute, other.second, other.microsecond) + return new + + def onOffset(self, dt): + if self.normalize and not _is_normalized(dt): + return False return date(dt.year, dt.month, dt.day) == easter(dt.year) #---------------------------------------------------------------------- # Ticks @@ -1935,11 +1986,8 @@ def delta(self): def nanos(self): return _delta_to_nanoseconds(self.delta) - @apply_nat + @apply_wraps def apply(self, other): - if type(other) == date: - other = datetime(other.year, other.month, other.day) - if isinstance(other, (datetime, timedelta)): return other + self.delta elif isinstance(other, type(self)): diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py index d3e306f3f956b..4fc7d281bc473 100644 --- a/pandas/tseries/tests/test_offsets.py +++ b/pandas/tseries/tests/test_offsets.py @@ -18,8 +18,8 @@ get_offset, get_offset_name, get_standard_freq) from pandas.tseries.frequencies import _offset_map -from pandas.tseries.index import _to_m8, DatetimeIndex, _daterange_cache -from pandas.tseries.tools import parse_time_string +from pandas.tseries.index import _to_m8, DatetimeIndex, _daterange_cache, date_range +from pandas.tseries.tools import parse_time_string, _maybe_get_tz import pandas.tseries.offsets as offsets from pandas.tslib import monthrange, OutOfBoundsDatetime, NaT @@ -100,21 +100,34 @@ def test_to_m8(): class Base(tm.TestCase): _offset = None - offset_types = [getattr(offsets, o) for o in offsets.__all__] - skip_np_u1p7 = [offsets.CustomBusinessDay, offsets.CDay, offsets.CustomBusinessMonthBegin, offsets.CustomBusinessMonthEnd, offsets.Nano] + _offset_types = [getattr(offsets, o) for o in offsets.__all__] + skip_np_u1p7 = [offsets.CustomBusinessDay, offsets.CDay, offsets.CustomBusinessMonthBegin, + offsets.CustomBusinessMonthEnd, offsets.Nano] - def _get_offset(self, klass, value=1): + @property + def offset_types(self): + if _np_version_under1p7: + return [o for o in self._offset_types if o not in self.skip_np_u1p7] + else: + return self._offset_types + + def _get_offset(self, klass, value=1, normalize=False): # create instance from offset class if klass is FY5253 or klass is FY5253Quarter: klass = klass(n=value, startingMonth=1, weekday=1, - qtr_with_extra_week=1, variation='last') - elif klass is WeekOfMonth or klass is LastWeekOfMonth: - klass = LastWeekOfMonth(n=value, weekday=5) + qtr_with_extra_week=1, variation='last', + normalize=normalize) + elif klass is LastWeekOfMonth: + klass = klass(n=value, weekday=5, normalize=normalize) + elif klass is WeekOfMonth: + klass = klass(n=value, week=1, weekday=5, normalize=normalize) + elif klass is Week: + klass = klass(n=value, weekday=5, normalize=normalize) else: try: - klass = klass(value) + klass = klass(value, normalize=normalize) except: - klass = klass() + klass = klass(normalize=normalize) return klass def test_apply_out_of_range(self): @@ -136,13 +149,45 @@ def test_apply_out_of_range(self): raise nose.SkipTest("cannot create out_of_range offset: {0} {1}".format(str(self).split('.')[-1],e)) -class TestOps(Base): +class TestCommon(Base): + + def setUp(self): + + # exected value created by Base._get_offset + # are applied to 2011/01/01 09:00 (Saturday) + # used for .apply and .rollforward + self.expecteds = {'Day': 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'), + 'CustomBusinessMonthBegin': Timestamp('2011-01-03 09:00:00'), + 'MonthBegin': Timestamp('2011-02-01 09:00:00'), + 'BusinessMonthBegin': Timestamp('2011-01-03 09:00:00'), + 'MonthEnd': Timestamp('2011-01-31 09:00:00'), + 'BusinessMonthEnd': Timestamp('2011-01-31 09:00:00'), + 'YearBegin': Timestamp('2012-01-01 09:00:00'), + 'BYearBegin': Timestamp('2011-01-03 09:00:00'), + 'YearEnd': Timestamp('2011-12-31 09:00:00'), + 'BYearEnd': Timestamp('2011-12-30 09:00:00'), + 'QuarterBegin': Timestamp('2011-03-01 09:00:00'), + 'BQuarterBegin': Timestamp('2011-03-01 09:00:00'), + 'QuarterEnd': Timestamp('2011-03-31 09:00:00'), + 'BQuarterEnd': Timestamp('2011-03-31 09:00:00'), + 'WeekOfMonth': Timestamp('2011-01-08 09:00:00'), + 'LastWeekOfMonth': Timestamp('2011-01-29 09:00:00'), + 'FY5253Quarter': Timestamp('2011-01-25 09:00:00'), + 'FY5253': Timestamp('2011-01-25 09:00:00'), + 'Week': Timestamp('2011-01-08 09:00:00'), + 'Easter': Timestamp('2011-04-24 09:00:00'), + 'Hour': Timestamp('2011-01-01 10:00:00'), + 'Minute': Timestamp('2011-01-01 09:01:00'), + 'Second': Timestamp('2011-01-01 09:00:01'), + 'Milli': Timestamp('2011-01-01 09:00:00.001000'), + 'Micro': Timestamp('2011-01-01 09:00:00.000001'), + 'Nano': Timestamp(np.datetime64('2011-01-01T09:00:00.000000001Z'))} def test_return_type(self): for offset in self.offset_types: - if _np_version_under1p7 and offset in self.skip_np_u1p7: - continue - offset = self._get_offset(offset) # make sure that we are returning a Timestamp @@ -156,6 +201,148 @@ def test_return_type(self): self.assertTrue(NaT - offset is NaT) self.assertTrue((-offset).apply(NaT) is NaT) + def _check_offsetfunc_works(self, offset, funcname, dt, expected, + normalize=False): + offset_s = self._get_offset(offset, normalize=normalize) + func = getattr(offset_s, funcname) + + result = func(dt) + self.assert_(isinstance(result, datetime)) + self.assertEqual(result, expected) + + result = func(Timestamp(dt)) + self.assert_(isinstance(result, Timestamp)) + self.assertEqual(result, expected) + + 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) + + def test_apply(self): + dt = datetime(2011, 1, 1, 9, 0) + + for offset in self.offset_types: + 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) + + 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'] + for n in no_changes: + expecteds[n] = Timestamp('2011/01/01 09:00') + + # but be changed when normalize=True + norm_expected = expecteds.copy() + for k in norm_expected: + norm_expected[k] = Timestamp(norm_expected[k].date()) + + normalized = {'Day': 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'), + 'Hour': Timestamp('2011-01-01 00:00:00'), + 'Minute': Timestamp('2011-01-01 00:00:00'), + 'Second': Timestamp('2011-01-01 00:00:00'), + 'Milli': Timestamp('2011-01-01 00:00:00'), + 'Micro': Timestamp('2011-01-01 00:00:00')} + norm_expected.update(normalized) + + dt = datetime(2011, 1, 1, 9, 0) + for offset in self.offset_types: + 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) + + def test_rollback(self): + expecteds = {'BusinessDay': Timestamp('2010-12-31 09:00:00'), + 'CustomBusinessDay': Timestamp('2010-12-31 09:00:00'), + 'CustomBusinessMonthEnd': Timestamp('2010-12-31 09:00:00'), + 'CustomBusinessMonthBegin': Timestamp('2010-12-01 09:00:00'), + 'BusinessMonthBegin': Timestamp('2010-12-01 09:00:00'), + 'MonthEnd': Timestamp('2010-12-31 09:00:00'), + 'BusinessMonthEnd': Timestamp('2010-12-31 09:00:00'), + 'BYearBegin': Timestamp('2010-01-01 09:00:00'), + 'YearEnd': Timestamp('2010-12-31 09:00:00'), + 'BYearEnd': Timestamp('2010-12-31 09:00:00'), + 'QuarterBegin': Timestamp('2010-12-01 09:00:00'), + 'BQuarterBegin': Timestamp('2010-12-01 09:00:00'), + 'QuarterEnd': Timestamp('2010-12-31 09:00:00'), + 'BQuarterEnd': Timestamp('2010-12-31 09:00:00'), + 'WeekOfMonth': Timestamp('2010-12-11 09:00:00'), + 'LastWeekOfMonth': Timestamp('2010-12-25 09:00:00'), + 'FY5253Quarter': Timestamp('2010-10-26 09:00:00'), + 'FY5253': Timestamp('2010-01-26 09:00:00'), + 'Easter': Timestamp('2010-04-04 09:00:00')} + + # 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']: + expecteds[n] = Timestamp('2011/01/01 09:00') + + # but be changed when normalize=True + norm_expected = expecteds.copy() + for k in norm_expected: + norm_expected[k] = Timestamp(norm_expected[k].date()) + + normalized = {'Day': 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'), + 'Hour': Timestamp('2011-01-01 00:00:00'), + 'Minute': Timestamp('2011-01-01 00:00:00'), + 'Second': Timestamp('2011-01-01 00:00:00'), + 'Milli': Timestamp('2011-01-01 00:00:00'), + 'Micro': Timestamp('2011-01-01 00:00:00')} + norm_expected.update(normalized) + + dt = datetime(2011, 1, 1, 9, 0) + for offset in self.offset_types: + expected = expecteds[offset.__name__] + + if offset == Nano: + self._check_nanofunc_works(offset, 'rollback', dt, expected) + else: + self._check_offsetfunc_works(offset, 'rollback', dt, expected) + + 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)) + + # 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)) + + date = datetime(dt.year, dt.month, dt.day) + self.assert_(offset_n.onOffset(date)) + class TestDateOffset(Base): _multiprocess_can_split_ = True @@ -1208,7 +1395,7 @@ def test_offset(self): def test_normalize(self): dt = datetime(2007, 1, 1, 3) - result = dt + BMonthEnd() + result = dt + BMonthEnd(normalize=True) expected = dt.replace(hour=0) + BMonthEnd() self.assertEqual(result, expected) @@ -1323,7 +1510,7 @@ def test_offset(self): def test_normalize(self): dt = datetime(2007, 1, 1, 3) - result = dt + MonthEnd() + result = dt + MonthEnd(normalize=True) expected = dt.replace(hour=0) + MonthEnd() self.assertEqual(result, expected)