From de0563616bb48443a5f6286e1b7726ff780ec694 Mon Sep 17 00:00:00 2001 From: Alex Rothberg Date: Thu, 22 Aug 2013 18:32:47 -0400 Subject: [PATCH] =?UTF-8?q?ENH:=20Support=20for=20"52=E2=80=9353-week=20fi?= =?UTF-8?q?scal=20year"=20/=20"4=E2=80=934=E2=80=935=20calendar"=20and=20L?= =?UTF-8?q?astWeekOfMonth=20DateOffset.=20-=20Added=20``LastWeekOfMonth``?= =?UTF-8?q?=20DateOffset=20-=20Added=20``FY5253=20and=20``FY5253Quarter``?= =?UTF-8?q?=20DateOffsets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/source/release.rst | 3 +- doc/source/timeseries.rst | 3 + doc/source/v0.13.0.txt | 2 + pandas/tseries/frequencies.py | 46 ++- pandas/tseries/offsets.py | 417 ++++++++++++++++++- pandas/tseries/tests/test_frequencies.py | 11 +- pandas/tseries/tests/test_offsets.py | 494 ++++++++++++++++++++++- pandas/tseries/tests/test_period.py | 6 +- pandas/tseries/tests/test_timeseries.py | 11 +- pandas/tseries/tests/test_tslib.py | 4 + pandas/tslib.pyx | 3 +- 11 files changed, 945 insertions(+), 55 deletions(-) diff --git a/doc/source/release.rst b/doc/source/release.rst index ed1834f14fc2e..a2015a3b361ac 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -62,7 +62,8 @@ New features - Auto-detect field widths in read_fwf when unspecified (:issue:`4488`) - ``to_csv()`` now outputs datetime objects according to a specified format string via the ``date_format`` keyword (:issue:`4313`) - + - Added ``LastWeekOfMonth`` DateOffset (:issue:`4637`) + - Added ``FY5253``, and ``FY5253Quarter`` DateOffsets (:issue:`4511`) Experimental Features ~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/source/timeseries.rst b/doc/source/timeseries.rst index cd12cc65dcd43..875ba2de93956 100644 --- a/doc/source/timeseries.rst +++ b/doc/source/timeseries.rst @@ -420,6 +420,7 @@ frequency increment. Specific offset logic like "month", "business day", or CDay, "custom business day (experimental)" Week, "one week, optionally anchored on a day of the week" WeekOfMonth, "the x-th day of the y-th week of each month" + LastWeekOfMonth, "the x-th day of the last week of each month" MonthEnd, "calendar month end" MonthBegin, "calendar month begin" BMonthEnd, "business month end" @@ -428,10 +429,12 @@ frequency increment. Specific offset logic like "month", "business day", or QuarterBegin, "calendar quarter begin" BQuarterEnd, "business quarter end" BQuarterBegin, "business quarter begin" + FY5253Quarter, "retail (aka 52-53 week) quarter" YearEnd, "calendar year end" YearBegin, "calendar year begin" BYearEnd, "business year end" BYearBegin, "business year begin" + FY5253, "retail (aka 52-53 week) year" Hour, "one hour" Minute, "one minute" Second, "one second" diff --git a/doc/source/v0.13.0.txt b/doc/source/v0.13.0.txt index 4c1ece032310f..02f231170ab97 100644 --- a/doc/source/v0.13.0.txt +++ b/doc/source/v0.13.0.txt @@ -511,6 +511,8 @@ Enhancements - Python csv parser now supports usecols (:issue:`4335`) - DataFrame has a new ``interpolate`` method, similar to Series (:issue:`4434`, :issue:`1892`) +- Added ``LastWeekOfMonth`` DateOffset (:issue:`4637`) +- Added ``FY5253``, and ``FY5253Quarter`` DateOffsets (:issue:`4511`) .. ipython:: python diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index 4878ebfccf915..cfe874484231b 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -95,6 +95,8 @@ def get_freq_code(freqstr): code = _period_str_to_code(freqstr[0]) stride = freqstr[1] except: + if com.is_integer(freqstr[1]): + raise code = _period_str_to_code(freqstr[1]) stride = freqstr[0] return code, stride @@ -227,10 +229,10 @@ def get_period_alias(offset_str): 'us': 'U' } +#TODO: Can this be killed? for _i, _weekday in enumerate(['MON', 'TUE', 'WED', 'THU', 'FRI']): for _iweek in range(4): _name = 'WOM-%d%s' % (_iweek + 1, _weekday) - _offset_map[_name] = offsets.WeekOfMonth(week=_iweek, weekday=_i) _rule_aliases[_name.replace('-', '@')] = _name # Note that _rule_aliases is not 1:1 (d[BA]==d[A@DEC]), and so traversal @@ -301,7 +303,7 @@ def to_offset(freqstr): # hack to handle WOM-1MON -opattern = re.compile(r'([\-]?\d*)\s*([A-Za-z]+([\-@]\d*[A-Za-z]+)?)') +opattern = re.compile(r'([\-]?\d*)\s*([A-Za-z]+([\-@][\dA-Za-z\-]+)?)') def _base_and_stride(freqstr): @@ -356,16 +358,16 @@ def get_offset(name): else: if name in _rule_aliases: name = _rule_aliases[name] - try: - if name not in _offset_map: + + if name not in _offset_map: + try: # generate and cache offset offset = _make_offset(name) - _offset_map[name] = offset - return _offset_map[name] - except (ValueError, TypeError, KeyError): - # bad prefix or suffix - pass - raise ValueError('Bad rule name requested: %s.' % name) + except (ValueError, TypeError, KeyError): + # bad prefix or suffix + raise ValueError('Bad rule name requested: %s.' % name) + _offset_map[name] = offset + return _offset_map[name] getOffset = get_offset @@ -401,9 +403,6 @@ def get_legacy_offset_name(offset): name = offset.name return _legacy_reverse_map.get(name, name) -get_offset_name = get_offset_name - - def get_standard_freq(freq): """ Return the standardized frequency string @@ -621,8 +620,12 @@ def _period_str_to_code(freqstr): try: freqstr = freqstr.upper() return _period_code_map[freqstr] - except: - alias = _period_alias_dict[freqstr] + except KeyError: + try: + alias = _period_alias_dict[freqstr] + except KeyError: + raise ValueError("Unknown freqstr: %s" % freqstr) + return _period_code_map[alias] @@ -839,16 +842,21 @@ def _get_monthly_rule(self): 'ce': 'M', 'be': 'BM'}.get(pos_check) def _get_wom_rule(self): - wdiffs = unique(np.diff(self.index.week)) - if not lib.ismember(wdiffs, set([4, 5])).all(): - return None +# wdiffs = unique(np.diff(self.index.week)) + #We also need -47, -49, -48 to catch index spanning year boundary +# if not lib.ismember(wdiffs, set([4, 5, -47, -49, -48])).all(): +# return None weekdays = unique(self.index.weekday) if len(weekdays) > 1: return None + + week_of_months = unique((self.index.day - 1) // 7) + if len(week_of_months) > 1: + return None # get which week - week = (self.index[0].day - 1) // 7 + 1 + week = week_of_months[0] + 1 wd = _weekday_rule_aliases[weekdays[0]] return 'WOM-%d%s' % (week, wd) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 9c60e77363b9b..07efbcfdcd7ba 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -6,7 +6,7 @@ from pandas.tseries.tools import to_datetime # import after tools, dateutil check -from dateutil.relativedelta import relativedelta +from dateutil.relativedelta import relativedelta, weekday import pandas.tslib as tslib from pandas.tslib import Timestamp from pandas import _np_version_under1p7 @@ -15,6 +15,7 @@ 'MonthBegin', 'BMonthBegin', 'MonthEnd', 'BMonthEnd', 'YearBegin', 'BYearBegin', 'YearEnd', 'BYearEnd', 'QuarterBegin', 'BQuarterBegin', 'QuarterEnd', 'BQuarterEnd', + 'LastWeekOfMonth', 'FY5253Quarter', 'FY5253', 'Week', 'WeekOfMonth', 'Hour', 'Minute', 'Second', 'Milli', 'Micro', 'Nano'] @@ -108,8 +109,8 @@ def _should_cache(self): def _params(self): attrs = [(k, v) for k, v in compat.iteritems(vars(self)) - if k not in ['kwds', '_offset', 'name', 'normalize', - 'busdaycalendar', '_named']] + if (k not in ['kwds', 'name', 'normalize', + 'busdaycalendar']) and (k[0] != '_')] attrs.extend(list(self.kwds.items())) attrs = sorted(set(attrs)) @@ -701,15 +702,23 @@ def _from_name(cls, suffix=None): weekday = _weekday_to_int[suffix] return cls(weekday=weekday) +class WeekDay(object): + MON = 0 + TUE = 1 + WED = 2 + THU = 3 + FRI = 4 + SAT = 5 + SUN = 6 _int_to_weekday = { - 0: 'MON', - 1: 'TUE', - 2: 'WED', - 3: 'THU', - 4: 'FRI', - 5: 'SAT', - 6: 'SUN' + WeekDay.MON: 'MON', + WeekDay.TUE: 'TUE', + WeekDay.WED: 'WED', + WeekDay.THU: 'THU', + WeekDay.FRI: 'FRI', + WeekDay.SAT: 'SAT', + WeekDay.SUN: 'SUN' } _weekday_to_int = dict((v, k) for k, v in _int_to_weekday.items()) @@ -800,6 +809,80 @@ def _from_name(cls, suffix=None): week = int(suffix[0]) - 1 weekday = _weekday_to_int[suffix[1:]] return cls(week=week, weekday=weekday) + +class LastWeekOfMonth(CacheableOffset, DateOffset): + """ + Describes monthly dates in last week of month like "the last Tuesday of each month" + + Parameters + ---------- + n : int + weekday : {0, 1, ..., 6} + 0: Mondays + 1: Tuesdays + 2: Wednesdays + 3: Thursdays + 4: Fridays + 5: Saturdays + 6: Sundays + """ + def __init__(self, n=1, **kwds): + self.n = n + self.weekday = kwds['weekday'] + + if self.n == 0: + raise ValueError('N cannot be 0') + + if self.weekday < 0 or self.weekday > 6: + raise ValueError('Day must be 0<=day<=6, got %d' % + self.weekday) + + self.kwds = kwds + + def apply(self, other): + offsetOfMonth = self.getOffsetOfMonth(other) + + if offsetOfMonth > other: + if self.n > 0: + months = self.n - 1 + else: + months = self.n + elif offsetOfMonth == other: + months = self.n + else: + if self.n > 0: + months = self.n + else: + months = self.n + 1 + + return self.getOffsetOfMonth(other + relativedelta(months=months, day=1)) + + def getOffsetOfMonth(self, dt): + m = MonthEnd() + d = datetime(dt.year, dt.month, 1) + + eom = m.rollforward(d) + + w = Week(weekday=self.weekday) + + return w.rollback(eom) + + def onOffset(self, dt): + return dt == self.getOffsetOfMonth(dt) + + @property + def rule_code(self): + return '%s-%s' % (self._prefix, _int_to_weekday.get(self.weekday, '')) + + _prefix = 'LWOM' + + @classmethod + def _from_name(cls, suffix=None): + if not suffix: + raise ValueError("Prefix %r requires a suffix." % (cls._prefix)) + # TODO: handle n here... + weekday = _weekday_to_int[suffix] + return cls(weekday=weekday) class QuarterOffset(DateOffset): @@ -876,7 +959,319 @@ def onOffset(self, dt): modMonth = (dt.month - self.startingMonth) % 3 return BMonthEnd().onOffset(dt) and modMonth == 0 +class FY5253(CacheableOffset, DateOffset): + """ + Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar. + + It is used by companies that desire that their + fiscal year always end on the same day of the week. + + It is a method of managing accounting periods. + It is a common calendar structure for some industries, + such as retail, manufacturing and parking industry. + + For more information see: + http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar + + + The year may either: + - end on the last X day of the Y month. + - end on the last X day closest to the last day of the Y month. + + X is a specific day of the week. + Y is a certain month of the year + + Parameters + ---------- + n : int + weekday : {0, 1, ..., 6} + 0: Mondays + 1: Tuesdays + 2: Wednesdays + 3: Thursdays + 4: Fridays + 5: Saturdays + 6: Sundays + startingMonth : The month in which fiscal years end. {1, 2, ... 12} + variation : str + {"nearest", "last"} for "LastOfMonth" or "NearestEndMonth" + """ + + _prefix = 'RE' + _suffix_prefix_last = 'L' + _suffix_prefix_nearest = 'N' + + def __init__(self, n=1, **kwds): + self.n = n + self.startingMonth = kwds['startingMonth'] + self.weekday = kwds["weekday"] + + self.variation = kwds["variation"] + + self.kwds = kwds + + if self.n == 0: + raise ValueError('N cannot be 0') + + if self.variation not in ["nearest", "last"]: + raise ValueError('%s is not a valid variation' % self.variation) + + if self.variation == "nearest": + self._rd_forward = relativedelta(weekday=weekday(self.weekday)) + self._rd_backward = relativedelta(weekday=weekday(self.weekday)(-1)) + else: + self._offset_lwom = LastWeekOfMonth(n=1, weekday=self.weekday) + + def isAnchored(self): + return self.n == 1 \ + and self.startingMonth is not None \ + and self.weekday is not None + + def onOffset(self, dt): + year_end = self.get_year_end(dt) + return year_end == dt + + def apply(self, other): + n = self.n + if n > 0: + year_end = self.get_year_end(other) + if other < year_end: + other = year_end + n -= 1 + elif other > year_end: + other = self.get_year_end(other + relativedelta(years=1)) + n -= 1 + + return self.get_year_end(other + relativedelta(years=n)) + else: + n = -n + year_end = self.get_year_end(other) + if other > year_end: + other = year_end + n -= 1 + elif other < year_end: + other = self.get_year_end(other + relativedelta(years=-1)) + n -= 1 + + return self.get_year_end(other + relativedelta(years=-n)) + + def get_year_end(self, dt): + if self.variation == "nearest": + return self._get_year_end_nearest(dt) + else: + return self._get_year_end_last(dt) + + def get_target_month_end(self, dt): + target_month = datetime(year=dt.year, month=self.startingMonth, day=1) + next_month_first_of = target_month + relativedelta(months=+1) + return next_month_first_of + relativedelta(days=-1) + + def _get_year_end_nearest(self, dt): + target_date = self.get_target_month_end(dt) + if target_date.weekday() == self.weekday: + return target_date + else: + forward = target_date + self._rd_forward + backward = target_date + self._rd_backward + + if forward - target_date < target_date - backward: + return forward + else: + return backward + + def _get_year_end_last(self, dt): + current_year = datetime(year=dt.year, month=self.startingMonth, day=1) + return current_year + self._offset_lwom + + @property + def rule_code(self): + suffix = self.get_rule_code_suffix() + return "%s-%s" % (self._get_prefix(), suffix) + + def _get_prefix(self): + return self._prefix + + def _get_suffix_prefix(self): + if self.variation == "nearest": + return self._suffix_prefix_nearest + else: + return self._suffix_prefix_last + + def get_rule_code_suffix(self): + return '%s-%s-%s' % (self._get_suffix_prefix(), \ + _int_to_month[self.startingMonth], \ + _int_to_weekday[self.weekday]) + + @classmethod + def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code): + if varion_code == "N": + variation = "nearest" + elif varion_code == "L": + variation = "last" + else: + raise ValueError("Unable to parse varion_code: %s" % (varion_code,)) + + startingMonth = _month_to_int[startingMonth_code] + weekday = _weekday_to_int[weekday_code] + + return { + "weekday":weekday, + "startingMonth":startingMonth, + "variation":variation, + } + + @classmethod + def _from_name(cls, *args): + return cls(**cls._parse_suffix(*args)) + +class FY5253Quarter(CacheableOffset, DateOffset): + """ + DateOffset increments between business quarter dates + for 52-53 week fiscal year (also known as a 4-4-5 calendar). + + It is used by companies that desire that their + fiscal year always end on the same day of the week. + + It is a method of managing accounting periods. + It is a common calendar structure for some industries, + such as retail, manufacturing and parking industry. + + For more information see: + http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar + + The year may either: + - end on the last X day of the Y month. + - end on the last X day closest to the last day of the Y month. + + X is a specific day of the week. + Y is a certain month of the year + + startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... + startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... + startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ... + + Parameters + ---------- + n : int + weekday : {0, 1, ..., 6} + 0: Mondays + 1: Tuesdays + 2: Wednesdays + 3: Thursdays + 4: Fridays + 5: Saturdays + 6: Sundays + startingMonth : The month in which fiscal years end. {1, 2, ... 12} + qtr_with_extra_week : The quarter number that has the leap + or 14 week when needed. {1,2,3,4} + variation : str + {"nearest", "last"} for "LastOfMonth" or "NearestEndMonth" + """ + + _prefix = 'REQ' + + def __init__(self, n=1, **kwds): + self.n = n + + self.qtr_with_extra_week = kwds["qtr_with_extra_week"] + + self.kwds = kwds + + if self.n == 0: + raise ValueError('N cannot be 0') + + self._offset = FY5253( \ + startingMonth=kwds['startingMonth'], \ + weekday=kwds["weekday"], + variation=kwds["variation"]) + + def isAnchored(self): + return self.n == 1 and self._offset.isAnchored() + + def apply(self, other): + n = self.n + + if n > 0: + while n > 0: + if not self._offset.onOffset(other): + qtr_lens = self.get_weeks(other) + start = other - self._offset + else: + start = other + qtr_lens = self.get_weeks(other + self._offset) + + for weeks in qtr_lens: + start += relativedelta(weeks=weeks) + if start > other: + other = start + n -= 1 + break + + else: + n = -n + while n > 0: + if not self._offset.onOffset(other): + qtr_lens = self.get_weeks(other) + end = other + self._offset + else: + end = other + qtr_lens = self.get_weeks(other) + + for weeks in reversed(qtr_lens): + end -= relativedelta(weeks=weeks) + if end < other: + other = end + n -= 1 + break + return other + + def get_weeks(self, dt): + ret = [13] * 4 + + year_has_extra_week = self.year_has_extra_week(dt) + + if year_has_extra_week: + ret[self.qtr_with_extra_week-1] = 14 + + return ret + + def year_has_extra_week(self, dt): + if self._offset.onOffset(dt): + prev_year_end = dt - self._offset + next_year_end = dt + else: + next_year_end = dt + self._offset + prev_year_end = dt - self._offset + + week_in_year = (next_year_end - prev_year_end).days/7 + + return week_in_year == 53 + + def onOffset(self, dt): + if self._offset.onOffset(dt): + return True + + next_year_end = dt - self._offset + + qtr_lens = self.get_weeks(dt) + + current = next_year_end + for qtr_len in qtr_lens[0:4]: + current += relativedelta(weeks=qtr_len) + if dt == current: + return True + return False + + @property + def rule_code(self): + suffix = self._offset.get_rule_code_suffix() + return "%s-%s" %(self._prefix, "%s-%d" % (suffix, self.qtr_with_extra_week)) + + @classmethod + def _from_name(cls, *args): + return cls(**dict(FY5253._parse_suffix(*args[:-1]), qtr_with_extra_week=int(args[-1]))) + _int_to_month = { 1: 'JAN', 2: 'FEB', @@ -1452,6 +1847,8 @@ def generate_range(start=None, end=None, periods=None, Hour, # 'H' Day, # 'D' WeekOfMonth, # 'WOM' + FY5253, + FY5253Quarter, ]) if not _np_version_under1p7: diff --git a/pandas/tseries/tests/test_frequencies.py b/pandas/tseries/tests/test_frequencies.py index 00a3d392a45c0..f1078f44efd13 100644 --- a/pandas/tseries/tests/test_frequencies.py +++ b/pandas/tseries/tests/test_frequencies.py @@ -148,17 +148,22 @@ def _check_tick(self, base_delta, code): self.assert_(infer_freq(index) is None) def test_weekly(self): - days = ['MON', 'TUE', 'WED', 'THU', 'FRI'] + days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] for day in days: self._check_generated_range('1/1/2000', 'W-%s' % day) def test_week_of_month(self): - days = ['MON', 'TUE', 'WED', 'THU', 'FRI'] + days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'] for day in days: for i in range(1, 5): self._check_generated_range('1/1/2000', 'WOM-%d%s' % (i, day)) + + def test_week_of_month_fake(self): + #All of these dates are on same day of week and are 4 or 5 weeks apart + index = DatetimeIndex(["2013-08-27","2013-10-01","2013-10-29","2013-11-26"]) + assert infer_freq(index) != 'WOM-4TUE' def test_monthly(self): self._check_generated_range('1/1/2000', 'M') @@ -195,7 +200,7 @@ def _check_generated_range(self, start, freq): gen = date_range(start, periods=7, freq=freq) index = _dti(gen.values) if not freq.startswith('Q-'): - self.assert_(infer_freq(index) == gen.freqstr) + self.assertEqual(infer_freq(index), gen.freqstr) else: inf_freq = infer_freq(index) self.assert_((inf_freq == 'Q-DEC' and diff --git a/pandas/tseries/tests/test_offsets.py b/pandas/tseries/tests/test_offsets.py index 8592a2c2d8d9c..7ebe6c0cfb728 100644 --- a/pandas/tseries/tests/test_offsets.py +++ b/pandas/tseries/tests/test_offsets.py @@ -1,4 +1,5 @@ from datetime import date, datetime, timedelta +from dateutil.relativedelta import relativedelta from pandas.compat import range from pandas import compat import unittest @@ -24,7 +25,8 @@ from pandas.lib import Timestamp from pandas.util.testing import assertRaisesRegexp import pandas.util.testing as tm -from pandas.tseries.offsets import BusinessMonthEnd, CacheableOffset +from pandas.tseries.offsets import BusinessMonthEnd, CacheableOffset, \ + LastWeekOfMonth, FY5253, FY5253Quarter, WeekDay from pandas import _np_version_under1p7 @@ -524,7 +526,9 @@ def test_weekmask_and_holidays(self): def assertOnOffset(offset, date, expected): actual = offset.onOffset(date) - assert actual == expected + assert actual == expected, ("\nExpected: %s\nActual: %s\nFor Offset: %s)" + "\nAt Date: %s" % + (expected, actual, offset, date)) class TestWeek(unittest.TestCase): @@ -674,6 +678,75 @@ def test_onOffset(self): offset = WeekOfMonth(week=week, weekday=weekday) self.assert_(offset.onOffset(date) == expected) +class TestLastWeekOfMonth(unittest.TestCase): + def test_constructor(self): + assertRaisesRegexp(ValueError, "^N cannot be 0", \ + LastWeekOfMonth, n=0, weekday=1) + + assertRaisesRegexp(ValueError, "^Day", LastWeekOfMonth, n=1, weekday=-1) + assertRaisesRegexp(ValueError, "^Day", LastWeekOfMonth, n=1, weekday=7) + + def test_offset(self): + #### Saturday + last_sat = datetime(2013,8,31) + next_sat = datetime(2013,9,28) + offset_sat = LastWeekOfMonth(n=1, weekday=5) + + one_day_before = (last_sat + timedelta(days=-1)) + self.assert_(one_day_before + offset_sat == last_sat) + + one_day_after = (last_sat + timedelta(days=+1)) + self.assert_(one_day_after + offset_sat == next_sat) + + #Test On that day + self.assert_(last_sat + offset_sat == next_sat) + + #### Thursday + + offset_thur = LastWeekOfMonth(n=1, weekday=3) + last_thurs = datetime(2013,1,31) + next_thurs = datetime(2013,2,28) + + one_day_before = last_thurs + timedelta(days=-1) + self.assert_(one_day_before + offset_thur == last_thurs) + + one_day_after = last_thurs + timedelta(days=+1) + self.assert_(one_day_after + offset_thur == next_thurs) + + # Test on that day + self.assert_(last_thurs + offset_thur == next_thurs) + + three_before = last_thurs + timedelta(days=-3) + self.assert_(three_before + offset_thur == last_thurs) + + two_after = last_thurs + timedelta(days=+2) + self.assert_(two_after + offset_thur == next_thurs) + + offset_sunday = LastWeekOfMonth(n=1, weekday=WeekDay.SUN) + self.assert_(datetime(2013,7,31) + offset_sunday == datetime(2013,8,25)) + + def test_onOffset(self): + test_cases = [ + (WeekDay.SUN, datetime(2013, 1, 27), True), + (WeekDay.SAT, datetime(2013, 3, 30), True), + (WeekDay.MON, datetime(2013, 2, 18), False), #Not the last Mon + (WeekDay.SUN, datetime(2013, 2, 25), False), #Not a SUN + (WeekDay.MON, datetime(2013, 2, 25), True), + (WeekDay.SAT, datetime(2013, 11, 30), True), + + (WeekDay.SAT, datetime(2006, 8, 26), True), + (WeekDay.SAT, datetime(2007, 8, 25), True), + (WeekDay.SAT, datetime(2008, 8, 30), True), + (WeekDay.SAT, datetime(2009, 8, 29), True), + (WeekDay.SAT, datetime(2010, 8, 28), True), + (WeekDay.SAT, datetime(2011, 8, 27), True), + (WeekDay.SAT, datetime(2019, 8, 31), True), + ] + + for weekday, date, expected in test_cases: + offset = LastWeekOfMonth(weekday=weekday) + self.assert_(offset.onOffset(date) == expected, date) + class TestBMonthBegin(unittest.TestCase): def test_offset(self): @@ -1101,7 +1174,379 @@ def test_onOffset(self): for offset, date, expected in tests: assertOnOffset(offset, date, expected) +def makeFY5253LastOfMonthQuarter(*args, **kwds): + return FY5253Quarter(*args, variation="last", **kwds) +def makeFY5253NearestEndMonthQuarter(*args, **kwds): + return FY5253Quarter(*args, variation="nearest", **kwds) + +def makeFY5253NearestEndMonth(*args, **kwds): + return FY5253(*args, variation="nearest", **kwds) + +def makeFY5253LastOfMonth(*args, **kwds): + return FY5253(*args, variation="last", **kwds) + +class TestFY5253LastOfMonth(unittest.TestCase): + def test_onOffset(self): + + offset_lom_sat_aug = makeFY5253LastOfMonth(1, startingMonth=8, weekday=WeekDay.SAT) + offset_lom_sat_sep = makeFY5253LastOfMonth(1, startingMonth=9, weekday=WeekDay.SAT) + + tests = [ + #From Wikipedia (see: http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar#Last_Saturday_of_the_month_at_fiscal_year_end) + (offset_lom_sat_aug, datetime(2006, 8, 26), True), + (offset_lom_sat_aug, datetime(2007, 8, 25), True), + (offset_lom_sat_aug, datetime(2008, 8, 30), True), + (offset_lom_sat_aug, datetime(2009, 8, 29), True), + (offset_lom_sat_aug, datetime(2010, 8, 28), True), + (offset_lom_sat_aug, datetime(2011, 8, 27), True), + (offset_lom_sat_aug, datetime(2012, 8, 25), True), + (offset_lom_sat_aug, datetime(2013, 8, 31), True), + (offset_lom_sat_aug, datetime(2014, 8, 30), True), + (offset_lom_sat_aug, datetime(2015, 8, 29), True), + (offset_lom_sat_aug, datetime(2016, 8, 27), True), + (offset_lom_sat_aug, datetime(2017, 8, 26), True), + (offset_lom_sat_aug, datetime(2018, 8, 25), True), + (offset_lom_sat_aug, datetime(2019, 8, 31), True), + + (offset_lom_sat_aug, datetime(2006, 8, 27), False), + (offset_lom_sat_aug, datetime(2007, 8, 28), False), + (offset_lom_sat_aug, datetime(2008, 8, 31), False), + (offset_lom_sat_aug, datetime(2009, 8, 30), False), + (offset_lom_sat_aug, datetime(2010, 8, 29), False), + (offset_lom_sat_aug, datetime(2011, 8, 28), False), + + (offset_lom_sat_aug, datetime(2006, 8, 25), False), + (offset_lom_sat_aug, datetime(2007, 8, 24), False), + (offset_lom_sat_aug, datetime(2008, 8, 29), False), + (offset_lom_sat_aug, datetime(2009, 8, 28), False), + (offset_lom_sat_aug, datetime(2010, 8, 27), False), + (offset_lom_sat_aug, datetime(2011, 8, 26), False), + (offset_lom_sat_aug, datetime(2019, 8, 30), False), + + #From GMCR (see for example: http://yahoo.brand.edgar-online.com/Default.aspx?companyid=3184&formtypeID=7) + (offset_lom_sat_sep, datetime(2010, 9, 25), True), + (offset_lom_sat_sep, datetime(2011, 9, 24), True), + (offset_lom_sat_sep, datetime(2012, 9, 29), True), + + ] + + for offset, date, expected in tests: + assertOnOffset(offset, date, expected) + + def test_apply(self): + offset_lom_aug_sat = makeFY5253LastOfMonth(startingMonth=8, weekday=WeekDay.SAT) + offset_lom_aug_sat_1 = makeFY5253LastOfMonth(n=1, startingMonth=8, weekday=WeekDay.SAT) + + date_seq_lom_aug_sat = [datetime(2006, 8, 26), datetime(2007, 8, 25), + datetime(2008, 8, 30), datetime(2009, 8, 29), + datetime(2010, 8, 28), datetime(2011, 8, 27), + datetime(2012, 8, 25), datetime(2013, 8, 31), + datetime(2014, 8, 30), datetime(2015, 8, 29), + datetime(2016, 8, 27)] + + tests = [ + (offset_lom_aug_sat, date_seq_lom_aug_sat), + (offset_lom_aug_sat_1, date_seq_lom_aug_sat), + (offset_lom_aug_sat, [datetime(2006, 8, 25)] + date_seq_lom_aug_sat), + (offset_lom_aug_sat_1, [datetime(2006, 8, 27)] + date_seq_lom_aug_sat[1:]), + (makeFY5253LastOfMonth(n=-1, startingMonth=8, weekday=WeekDay.SAT), list(reversed(date_seq_lom_aug_sat))), + ] + for test in tests: + offset, data = test + current = data[0] + for datum in data[1:]: + current = current + offset + self.assertEqual(current, datum) + +class TestFY5253NearestEndMonth(unittest.TestCase): + def test_get_target_month_end(self): + self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SAT).get_target_month_end(datetime(2013,1,1)), datetime(2013,8,31)) + self.assertEqual(makeFY5253NearestEndMonth(startingMonth=12, weekday=WeekDay.SAT).get_target_month_end(datetime(2013,1,1)), datetime(2013,12,31)) + self.assertEqual(makeFY5253NearestEndMonth(startingMonth=2, weekday=WeekDay.SAT).get_target_month_end(datetime(2013,1,1)), datetime(2013,2,28)) + + def test_get_year_end(self): + self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SAT).get_year_end(datetime(2013,1,1)), datetime(2013,8,31)) + self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SUN).get_year_end(datetime(2013,1,1)), datetime(2013,9,1)) + self.assertEqual(makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.FRI).get_year_end(datetime(2013,1,1)), datetime(2013,8,30)) + + def test_onOffset(self): + offset_lom_aug_sat = makeFY5253NearestEndMonth(1, startingMonth=8, weekday=WeekDay.SAT) + offset_lom_aug_thu = makeFY5253NearestEndMonth(1, startingMonth=8, weekday=WeekDay.THU) + + tests = [ +# From Wikipedia (see: http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar#Saturday_nearest_the_end_of_month) +# 2006-09-02 2006 September 2 +# 2007-09-01 2007 September 1 +# 2008-08-30 2008 August 30 (leap year) +# 2009-08-29 2009 August 29 +# 2010-08-28 2010 August 28 +# 2011-09-03 2011 September 3 +# 2012-09-01 2012 September 1 (leap year) +# 2013-08-31 2013 August 31 +# 2014-08-30 2014 August 30 +# 2015-08-29 2015 August 29 +# 2016-09-03 2016 September 3 (leap year) +# 2017-09-02 2017 September 2 +# 2018-09-01 2018 September 1 +# 2019-08-31 2019 August 31 + (offset_lom_aug_sat, datetime(2006, 9, 2), True), + (offset_lom_aug_sat, datetime(2007, 9, 1), True), + (offset_lom_aug_sat, datetime(2008, 8, 30), True), + (offset_lom_aug_sat, datetime(2009, 8, 29), True), + (offset_lom_aug_sat, datetime(2010, 8, 28), True), + (offset_lom_aug_sat, datetime(2011, 9, 3), True), + + (offset_lom_aug_sat, datetime(2016, 9, 3), True), + (offset_lom_aug_sat, datetime(2017, 9, 2), True), + (offset_lom_aug_sat, datetime(2018, 9, 1), True), + (offset_lom_aug_sat, datetime(2019, 8, 31), True), + + (offset_lom_aug_sat, datetime(2006, 8, 27), False), + (offset_lom_aug_sat, datetime(2007, 8, 28), False), + (offset_lom_aug_sat, datetime(2008, 8, 31), False), + (offset_lom_aug_sat, datetime(2009, 8, 30), False), + (offset_lom_aug_sat, datetime(2010, 8, 29), False), + (offset_lom_aug_sat, datetime(2011, 8, 28), False), + + (offset_lom_aug_sat, datetime(2006, 8, 25), False), + (offset_lom_aug_sat, datetime(2007, 8, 24), False), + (offset_lom_aug_sat, datetime(2008, 8, 29), False), + (offset_lom_aug_sat, datetime(2009, 8, 28), False), + (offset_lom_aug_sat, datetime(2010, 8, 27), False), + (offset_lom_aug_sat, datetime(2011, 8, 26), False), + (offset_lom_aug_sat, datetime(2019, 8, 30), False), + + #From Micron, see: http://google.brand.edgar-online.com/?sym=MU&formtypeID=7 + (offset_lom_aug_thu, datetime(2012, 8, 30), True), + (offset_lom_aug_thu, datetime(2011, 9, 1), True), + + ] + + for offset, date, expected in tests: + assertOnOffset(offset, date, expected) + + def test_apply(self): + date_seq_nem_8_sat = [datetime(2006, 9, 2), datetime(2007, 9, 1), datetime(2008, 8, 30), datetime(2009, 8, 29), datetime(2010, 8, 28), datetime(2011, 9, 3)] + + tests = [ + (makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SAT), date_seq_nem_8_sat), + (makeFY5253NearestEndMonth(n=1, startingMonth=8, weekday=WeekDay.SAT), date_seq_nem_8_sat), + (makeFY5253NearestEndMonth(startingMonth=8, weekday=WeekDay.SAT), [datetime(2006, 9, 1)] + date_seq_nem_8_sat), + (makeFY5253NearestEndMonth(n=1, startingMonth=8, weekday=WeekDay.SAT), [datetime(2006, 9, 3)] + date_seq_nem_8_sat[1:]), + (makeFY5253NearestEndMonth(n=-1, startingMonth=8, weekday=WeekDay.SAT), list(reversed(date_seq_nem_8_sat))), + ] + for test in tests: + offset, data = test + current = data[0] + for datum in data[1:]: + current = current + offset + self.assertEqual(current, datum) + +class TestFY5253LastOfMonthQuarter(unittest.TestCase): + + def test_isAnchored(self): + self.assert_(makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4).isAnchored()) + self.assert_(makeFY5253LastOfMonthQuarter(weekday=WeekDay.SAT, startingMonth=3, qtr_with_extra_week=4).isAnchored()) + self.assert_(not makeFY5253LastOfMonthQuarter(2, startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4).isAnchored()) + + def test_equality(self): + self.assertEqual(makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4), makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4)) + self.assertNotEqual(makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4), makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SUN, qtr_with_extra_week=4)) + self.assertNotEqual(makeFY5253LastOfMonthQuarter(startingMonth=1, weekday=WeekDay.SAT, qtr_with_extra_week=4), makeFY5253LastOfMonthQuarter(startingMonth=2, weekday=WeekDay.SAT, qtr_with_extra_week=4)) + + def test_offset(self): + offset = makeFY5253LastOfMonthQuarter(1, startingMonth=9, weekday=WeekDay.SAT, qtr_with_extra_week=4) + offset2 = makeFY5253LastOfMonthQuarter(2, startingMonth=9, weekday=WeekDay.SAT, qtr_with_extra_week=4) + offset4 = makeFY5253LastOfMonthQuarter(4, startingMonth=9, weekday=WeekDay.SAT, qtr_with_extra_week=4) + + offset_neg1 = makeFY5253LastOfMonthQuarter(-1, startingMonth=9, weekday=WeekDay.SAT, qtr_with_extra_week=4) + offset_neg2 = makeFY5253LastOfMonthQuarter(-2, startingMonth=9, weekday=WeekDay.SAT, qtr_with_extra_week=4) + + GMCR = [datetime(2010, 3, 27), + datetime(2010, 6, 26), + datetime(2010, 9, 25), + datetime(2010, 12, 25), + datetime(2011, 3, 26), + datetime(2011, 6, 25), + datetime(2011, 9, 24), + datetime(2011, 12, 24), + datetime(2012, 3, 24), + datetime(2012, 6, 23), + datetime(2012, 9, 29), + datetime(2012, 12, 29), + datetime(2013, 3, 30), + datetime(2013, 6, 29)] + + + assertEq(offset, base=GMCR[0], expected=GMCR[1]) + assertEq(offset, base=GMCR[0] + relativedelta(days=-1), expected=GMCR[0]) + assertEq(offset, base=GMCR[1], expected=GMCR[2]) + + assertEq(offset2, base=GMCR[0], expected=GMCR[2]) + assertEq(offset4, base=GMCR[0], expected=GMCR[4]) + + assertEq(offset_neg1, base=GMCR[-1], expected=GMCR[-2]) + assertEq(offset_neg1, base=GMCR[-1] + relativedelta(days=+1), expected=GMCR[-1]) + assertEq(offset_neg2, base=GMCR[-1], expected=GMCR[-3]) + + date = GMCR[0] + relativedelta(days=-1) + for expected in GMCR: + assertEq(offset, date, expected) + date = date + offset + + date = GMCR[-1] + relativedelta(days=+1) + for expected in reversed(GMCR): + assertEq(offset_neg1, date, expected) + date = date + offset_neg1 + + + def test_onOffset(self): + lomq_aug_sat_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=8, weekday=WeekDay.SAT, qtr_with_extra_week=4) + lomq_sep_sat_4 = makeFY5253LastOfMonthQuarter(1, startingMonth=9, weekday=WeekDay.SAT, qtr_with_extra_week=4) + + tests = [ + #From Wikipedia + (lomq_aug_sat_4, datetime(2006, 8, 26), True), + (lomq_aug_sat_4, datetime(2007, 8, 25), True), + (lomq_aug_sat_4, datetime(2008, 8, 30), True), + (lomq_aug_sat_4, datetime(2009, 8, 29), True), + (lomq_aug_sat_4, datetime(2010, 8, 28), True), + (lomq_aug_sat_4, datetime(2011, 8, 27), True), + (lomq_aug_sat_4, datetime(2019, 8, 31), True), + + (lomq_aug_sat_4, datetime(2006, 8, 27), False), + (lomq_aug_sat_4, datetime(2007, 8, 28), False), + (lomq_aug_sat_4, datetime(2008, 8, 31), False), + (lomq_aug_sat_4, datetime(2009, 8, 30), False), + (lomq_aug_sat_4, datetime(2010, 8, 29), False), + (lomq_aug_sat_4, datetime(2011, 8, 28), False), + + (lomq_aug_sat_4, datetime(2006, 8, 25), False), + (lomq_aug_sat_4, datetime(2007, 8, 24), False), + (lomq_aug_sat_4, datetime(2008, 8, 29), False), + (lomq_aug_sat_4, datetime(2009, 8, 28), False), + (lomq_aug_sat_4, datetime(2010, 8, 27), False), + (lomq_aug_sat_4, datetime(2011, 8, 26), False), + (lomq_aug_sat_4, datetime(2019, 8, 30), False), + + #From GMCR + (lomq_sep_sat_4, datetime(2010, 9, 25), True), + (lomq_sep_sat_4, datetime(2011, 9, 24), True), + (lomq_sep_sat_4, datetime(2012, 9, 29), True), + + (lomq_sep_sat_4, datetime(2013, 6, 29), True), + (lomq_sep_sat_4, datetime(2012, 6, 23), True), + (lomq_sep_sat_4, datetime(2012, 6, 30), False), + + (lomq_sep_sat_4, datetime(2013, 3, 30), True), + (lomq_sep_sat_4, datetime(2012, 3, 24), True), + + (lomq_sep_sat_4, datetime(2012, 12, 29), True), + (lomq_sep_sat_4, datetime(2011, 12, 24), True), + + #INTC (extra week in Q1) + #See: http://www.intc.com/releasedetail.cfm?ReleaseID=542844 + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1), datetime(2011, 4, 2), True), + + #see: http://google.brand.edgar-online.com/?sym=INTC&formtypeID=7 + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1), datetime(2012, 12, 29), True), + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1), datetime(2011, 12, 31), True), + (makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1), datetime(2010, 12, 25), True), + + ] + + for offset, date, expected in tests: + assertOnOffset(offset, date, expected) + + def test_year_has_extra_week(self): + #End of long Q1 + self.assertTrue(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(2011, 4, 2))) + + #Start of long Q1 + self.assertTrue(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(2010, 12, 26))) + + #End of year before year with long Q1 + self.assertFalse(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(2010, 12, 25))) + + for year in [x for x in range(1994, 2011+1) if x not in [2011, 2005, 2000, 1994]]: + self.assertFalse(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(year, 4, 2))) + + #Other long years + self.assertTrue(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(2005, 4, 2))) + self.assertTrue(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(2000, 4, 2))) + self.assertTrue(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).year_has_extra_week(datetime(1994, 4, 2))) + + def test_get_weeks(self): + self.assertEqual(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).get_weeks(datetime(2011, 4, 2)), [14, 13, 13, 13]) + self.assertEqual(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=4).get_weeks(datetime(2011, 4, 2)), [13, 13, 13, 14]) + self.assertEqual(makeFY5253LastOfMonthQuarter(1, startingMonth=12, weekday=WeekDay.SAT, qtr_with_extra_week=1).get_weeks(datetime(2010, 12, 25)), [13, 13, 13, 13]) + +class TestFY5253NearestEndMonthQuarter(unittest.TestCase): + + def test_onOffset(self): + + offset_nem_sat_aug_4 = makeFY5253NearestEndMonthQuarter(1, startingMonth=8, weekday=WeekDay.SAT, qtr_with_extra_week=4) + offset_nem_thu_aug_4 = makeFY5253NearestEndMonthQuarter(1, startingMonth=8, weekday=WeekDay.THU, qtr_with_extra_week=4) + tests = [ + #From Wikipedia + (offset_nem_sat_aug_4, datetime(2006, 9, 2), True), + (offset_nem_sat_aug_4, datetime(2007, 9, 1), True), + (offset_nem_sat_aug_4, datetime(2008, 8, 30), True), + (offset_nem_sat_aug_4, datetime(2009, 8, 29), True), + (offset_nem_sat_aug_4, datetime(2010, 8, 28), True), + (offset_nem_sat_aug_4, datetime(2011, 9, 3), True), + + (offset_nem_sat_aug_4, datetime(2016, 9, 3), True), + (offset_nem_sat_aug_4, datetime(2017, 9, 2), True), + (offset_nem_sat_aug_4, datetime(2018, 9, 1), True), + (offset_nem_sat_aug_4, datetime(2019, 8, 31), True), + + (offset_nem_sat_aug_4, datetime(2006, 8, 27), False), + (offset_nem_sat_aug_4, datetime(2007, 8, 28), False), + (offset_nem_sat_aug_4, datetime(2008, 8, 31), False), + (offset_nem_sat_aug_4, datetime(2009, 8, 30), False), + (offset_nem_sat_aug_4, datetime(2010, 8, 29), False), + (offset_nem_sat_aug_4, datetime(2011, 8, 28), False), + + (offset_nem_sat_aug_4, datetime(2006, 8, 25), False), + (offset_nem_sat_aug_4, datetime(2007, 8, 24), False), + (offset_nem_sat_aug_4, datetime(2008, 8, 29), False), + (offset_nem_sat_aug_4, datetime(2009, 8, 28), False), + (offset_nem_sat_aug_4, datetime(2010, 8, 27), False), + (offset_nem_sat_aug_4, datetime(2011, 8, 26), False), + (offset_nem_sat_aug_4, datetime(2019, 8, 30), False), + + #From Micron, see: http://google.brand.edgar-online.com/?sym=MU&formtypeID=7 + (offset_nem_thu_aug_4, datetime(2012, 8, 30), True), + (offset_nem_thu_aug_4, datetime(2011, 9, 1), True), + + #See: http://google.brand.edgar-online.com/?sym=MU&formtypeID=13 + (offset_nem_thu_aug_4, datetime(2013, 5, 30), True), + (offset_nem_thu_aug_4, datetime(2013, 2, 28), True), + (offset_nem_thu_aug_4, datetime(2012, 11, 29), True), + (offset_nem_thu_aug_4, datetime(2012, 5, 31), True), + (offset_nem_thu_aug_4, datetime(2007, 3, 1), True), + (offset_nem_thu_aug_4, datetime(1994, 3, 3), True), + + ] + + for offset, date, expected in tests: + assertOnOffset(offset, date, expected) + + def test_offset(self): + offset = makeFY5253NearestEndMonthQuarter(1, startingMonth=8, weekday=WeekDay.THU, qtr_with_extra_week=4) + + MU = [datetime(2012, 5, 31), datetime(2012, 8, 30), datetime(2012, 11, 29), datetime(2013, 2, 28), datetime(2013, 5, 30)] + + date = MU[0] + relativedelta(days=-1) + for expected in MU: + assertEq(offset, date, expected) + date = date + offset + + assertEq(offset, datetime(2012, 5, 31), datetime(2012, 8, 30)) + assertEq(offset, datetime(2012, 5, 30), datetime(2012, 5, 31)) + class TestQuarterBegin(unittest.TestCase): def test_repr(self): self.assertEqual(repr(QuarterBegin()), "") @@ -1748,32 +2193,43 @@ def test_compare_ticks(): assert(kls(3) != kls(4)) -def test_get_offset_name(): - assertRaisesRegexp(ValueError, 'Bad rule.*BusinessDays', get_offset_name, BDay(2)) - - assert get_offset_name(BDay()) == 'B' - assert get_offset_name(BMonthEnd()) == 'BM' - assert get_offset_name(Week(weekday=0)) == 'W-MON' - assert get_offset_name(Week(weekday=1)) == 'W-TUE' - assert get_offset_name(Week(weekday=2)) == 'W-WED' - assert get_offset_name(Week(weekday=3)) == 'W-THU' - assert get_offset_name(Week(weekday=4)) == 'W-FRI' +class TestOffsetNames(unittest.TestCase): + def test_get_offset_name(self): + assertRaisesRegexp(ValueError, 'Bad rule.*BusinessDays', get_offset_name, BDay(2)) + + assert get_offset_name(BDay()) == 'B' + assert get_offset_name(BMonthEnd()) == 'BM' + assert get_offset_name(Week(weekday=0)) == 'W-MON' + assert get_offset_name(Week(weekday=1)) == 'W-TUE' + assert get_offset_name(Week(weekday=2)) == 'W-WED' + assert get_offset_name(Week(weekday=3)) == 'W-THU' + assert get_offset_name(Week(weekday=4)) == 'W-FRI' + self.assertEqual(get_offset_name(LastWeekOfMonth(weekday=WeekDay.SUN)), "LWOM-SUN") + self.assertEqual(get_offset_name(makeFY5253LastOfMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=4)),"REQ-L-MAR-TUE-4") + self.assertEqual(get_offset_name(makeFY5253NearestEndMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=3)), "REQ-N-MAR-TUE-3") def test_get_offset(): assertRaisesRegexp(ValueError, "rule.*GIBBERISH", get_offset, 'gibberish') assertRaisesRegexp(ValueError, "rule.*QS-JAN-B", get_offset, 'QS-JAN-B') - pairs = [('B', BDay()), ('b', BDay()), ('bm', BMonthEnd()), + pairs = [ + ('B', BDay()), ('b', BDay()), ('bm', BMonthEnd()), ('Bm', BMonthEnd()), ('W-MON', Week(weekday=0)), ('W-TUE', Week(weekday=1)), ('W-WED', Week(weekday=2)), ('W-THU', Week(weekday=3)), ('W-FRI', Week(weekday=4)), - ('w@Sat', Week(weekday=5))] + ('w@Sat', Week(weekday=5)), + ("RE-N-DEC-MON", makeFY5253NearestEndMonth(weekday=0, startingMonth=12)), + ("RE-L-DEC-TUE", makeFY5253LastOfMonth(weekday=1, startingMonth=12)), + ("REQ-L-MAR-TUE-4", makeFY5253LastOfMonthQuarter(weekday=1, startingMonth=3, qtr_with_extra_week=4)), + ("REQ-L-DEC-MON-3", makeFY5253LastOfMonthQuarter(weekday=0, startingMonth=12, qtr_with_extra_week=3)), + ("REQ-N-DEC-MON-3", makeFY5253NearestEndMonthQuarter(weekday=0, startingMonth=12, qtr_with_extra_week=3)), + ] for name, expected in pairs: offset = get_offset(name) assert offset == expected, ("Expected %r to yield %r (actual: %r)" % (name, expected, offset)) - + def test_parse_time_string(): (date, parsed, reso) = parse_time_string('4Q1984') @@ -1879,8 +2335,11 @@ def get_all_subclasses(cls): ret | get_all_subclasses(this_subclass) return ret - class TestCaching(unittest.TestCase): + no_simple_ctr = [WeekOfMonth, FY5253, + FY5253Quarter, + LastWeekOfMonth] + def test_should_cache_month_end(self): self.assertTrue(MonthEnd()._should_cache()) @@ -1892,7 +2351,8 @@ def test_should_cache_week_month(self): def test_all_cacheableoffsets(self): for subclass in get_all_subclasses(CacheableOffset): - if subclass in [WeekOfMonth]: + if subclass.__name__[0] == "_" \ + or subclass in TestCaching.no_simple_ctr: continue self.run_X_index_creation(subclass) diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py index 0fc7101a99856..312a88bcbc5a9 100644 --- a/pandas/tseries/tests/test_period.py +++ b/pandas/tseries/tests/test_period.py @@ -199,7 +199,7 @@ def test_period_constructor(self): self.assertRaises(ValueError, Period, ordinal=200701) - self.assertRaises(KeyError, Period, '2007-1-1', freq='X') + self.assertRaises(ValueError, Period, '2007-1-1', freq='X') def test_freq_str(self): i1 = Period('1982', freq='Min') @@ -1136,8 +1136,8 @@ def test_constructor_field_arrays(self): self.assert_(idx.equals(exp)) def test_constructor_U(self): - # X was used as undefined period - self.assertRaises(KeyError, period_range, '2007-1-1', periods=500, + # U was used as undefined period + self.assertRaises(ValueError, period_range, '2007-1-1', periods=500, freq='X') def test_constructor_arrays_negative_year(self): diff --git a/pandas/tseries/tests/test_timeseries.py b/pandas/tseries/tests/test_timeseries.py index 7f11fa5873fe7..dee0587aaaa02 100644 --- a/pandas/tseries/tests/test_timeseries.py +++ b/pandas/tseries/tests/test_timeseries.py @@ -2664,7 +2664,7 @@ def test_frequency_misc(self): expected = offsets.Minute(5) self.assertEquals(result, expected) - self.assertRaises(KeyError, fmod.get_freq_code, (5, 'baz')) + self.assertRaises(ValueError, fmod.get_freq_code, (5, 'baz')) self.assertRaises(ValueError, fmod.to_offset, '100foo') @@ -3031,6 +3031,15 @@ def test_frame_apply_dont_convert_datetime64(self): df = df.applymap(lambda x: x + BDay()) self.assertTrue(df.x1.dtype == 'M8[ns]') + + def test_date_range_fy5252(self): + dr = date_range(start="2013-01-01", + periods=2, + freq=offsets.FY5253(startingMonth=1, + weekday=3, + variation="nearest")) + self.assertEqual(dr[0], Timestamp('2013-01-31')) + self.assertEqual(dr[1], Timestamp('2014-01-30')) if __name__ == '__main__': diff --git a/pandas/tseries/tests/test_tslib.py b/pandas/tseries/tests/test_tslib.py index cfc93a22c454b..40dbb2d3712af 100644 --- a/pandas/tseries/tests/test_tslib.py +++ b/pandas/tseries/tests/test_tslib.py @@ -283,6 +283,10 @@ def test_period_ordinal_business_day(self): # Tuesday self.assertEqual(11418, period_ordinal(2013, 10, 8, 0, 0, 0, 0, 0, get_freq('B'))) +class TestTomeStampOps(unittest.TestCase): + def test_timestamp_and_datetime(self): + self.assertEqual((Timestamp(datetime.datetime(2013, 10,13)) - datetime.datetime(2013, 10,12)).days, 1) + self.assertEqual((datetime.datetime(2013, 10, 12) - Timestamp(datetime.datetime(2013, 10,13))).days, -1) if __name__ == '__main__': nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'], diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index d95956261bc44..c487202c4c0f9 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -609,7 +609,8 @@ cdef class _Timestamp(datetime): if is_integer_object(other): neg_other = -other return self + neg_other - return super(_Timestamp, self).__sub__(other) + # This calling convention is required + return datetime.__sub__(self, other) cpdef _get_field(self, field): out = get_date_field(np.array([self.value], dtype=np.int64), field)