# -*- coding: utf-8 -*-
# businessdate
# ------------
# A fast, efficient Python library for generating business dates inherited
# from float for fast date operations. Typical banking business methods
# are provided like business holidays adjustment, day count fractions.
# Beside dates generic business periods offer to create time periods like
# '10Y', '3 Months' or '2b'. Periods can easily added to business dates.
#
# Author: pbrisk <pbrisk@icloud.com>
# Copyright: 2016, 2017 Deutsche Postbank AG
# Website: https://github.com/pbrisk/businessdate
# License: APACHE Version 2 License (see LICENSE file)
from calendar import monthrange, weekday, WEDNESDAY, FRIDAY
from copy import copy
from datetime import date, datetime, timedelta
from basedate import BaseDate, BaseDateFloat, BaseDatetimeDate, \
is_leap_year, is_valid_ymd, days_in_year, days_in_month, \
from_ymd_to_excel, from_excel_to_ymd
from baseperiod import BasePeriod
#: int: Excel representation of 31.12.2015
BASE_DATE = 42369
#: string: basic date format as string
DATE_FORMAT = '%Y%m%d'
[docs]class BusinessHolidays(list):
"""
holiday calendar class
"""
def __init__(self, iterable=None):
"""
:param iterable iterable: sequence of holiday dates
"""
if iterable:
super(BusinessHolidays, self).__init__(BusinessDate(iterable))
else:
super(BusinessHolidays, self).__init__()
def __contains__(self, item):
if super(BusinessHolidays, self).__contains__(item):
return True
target_days_in_year = target_days(item.year)
self.extend(target_days_in_year.keys())
if item in target_days_in_year:
return True
else:
return False
#: list: list of dates of default holiday calendar
DEFAULT_HOLIDAYS = BusinessHolidays()
[docs]class BusinessDate(BaseDate):
def __new__(cls, date_value=None):
r"""
fundamental date class
:param date_value: input value to create BusinessDate instance
:type date_value: int, float, string or datetime.date
:return: BusinessDate
creates new BusinessDate either from `int`, `float`, `string`, `datetime.date`
therefore the following will create the same
.. code-block:: python
BusinessDate(datetime.date(2015, 12, 31))
BusinessDate(20151231)
BusinessDate(2015-12-31)
BusinessDate(31.12.2015)
BusinessDate(12/31/2015)
BusinessDate(42369)
BusinessDate(42369.0)
BusinessDate(735963)
BusinessDate(735963.0)
BusinessDate()
**caution:** recommended is the use of classmethods BusinessDate.from_string, from_date etc.
"""
if date_value is None:
new_date = BusinessDate.from_excel(BASE_DATE)
elif isinstance(date_value, BaseDateFloat):
return super(BusinessDate, cls).__new__(cls, float(date_value))
elif isinstance(date_value, BaseDatetimeDate):
return super(BusinessDate, cls).__new__(cls, date_value.year, date_value.month, date_value.day)
elif isinstance(date_value, (int, float)):
if date_value >= 18990101:
new_date = BusinessDate.from_string(str(date_value))
elif 1 < date_value < 200 * 365.25:
new_date = BusinessDate.from_excel(date_value)
else:
new_date = BusinessDate.from_ordinal(date_value)
elif isinstance(date_value, str):
new_date = BusinessDate.from_string(date_value)
elif isinstance(date_value, (date, datetime)):
new_date = BusinessDate.from_date(date_value)
elif isinstance(date_value, list):
new_date = [BusinessDate(d) for d in date_value]
else:
raise ValueError("Can't build BusinessDate form %s type" % str(type(date_value)))
return new_date
# --- operator methods ---------------------------------------------------
def __add__(self, other):
"""
addition of BusinessDate.
:param BusinessDate or BusinessPeriod or str other: can be BusinessPeriod or
any thing that might be casted to it.
"""
if isinstance(other, BusinessPeriod):
return self.add_period(other)
elif BusinessPeriod.is_businessperiod(other):
return self + BusinessPeriod(other)
else:
raise TypeError('substraction of BusinessDates cannot handle objects of type %s.' % type(other))
def __sub__(self, other):
"""
subtraction of BusinessDate.
:param object other: can be other BusinessDate, BusinessPeriod or any thing that might be casted to those.
"""
if isinstance(other, BusinessDate):
y, m, d = self.diff(other)
return BusinessPeriod(years=y, months=m, days=d)
elif isinstance(other, BusinessPeriod):
return self.add_period(-1 * other)
elif BusinessDate.is_businessdate(other):
return self - BusinessDate(other)
elif BusinessPeriod.is_businessperiod(other):
return self - BusinessPeriod(other)
else:
raise TypeError('substraction of BusinessDates connot handle objects of type %s.' % type(other))
def __str__(self):
# return self.__class__.__name__ + '(' + self.to_string() + ')'
return self.to_string()
def __repr__(self):
return self.to_string()
# --- constructor methods ------------------------------------------------
@staticmethod
[docs] def from_ymd(y, m, d):
# return super(BusinessDate, cls).__new__(BaseDate.from_ymd(y, m, d))
return BusinessDate(BaseDate.from_ymd(y, m, d))
@staticmethod
[docs] def from_bankdate(d):
# deprecated
return BusinessDate.from_businessdate(d)
@staticmethod
[docs] def from_businessdate(d):
"""
copy constructor
:param BusinessDate d:
:return bankdate:
"""
return copy(d)
@staticmethod
[docs] def from_excel(excel_int):
y, m, d = from_excel_to_ymd(excel_int)
return BusinessDate.from_ymd(y, m, d)
@staticmethod
[docs] def from_date(datetime_date):
"""
construct BusinessDate instance from datetime.date instance,
raise ValueError exception if not possible
:param datetime.date datetime_date: calendar day
:return bool:
"""
return BusinessDate.from_ymd(datetime_date.year, datetime_date.month, datetime_date.day)
@staticmethod
[docs] def from_ordinal(ordinal_int):
d = date.fromordinal(int(ordinal_int))
return BusinessDate.from_ymd(d.year, d.month, d.day)
@staticmethod
[docs] def from_string(date_str):
"""
construction from the following string patterns
'%Y-%m-%d'
'%d.%m.%Y'
'%m/%d/%Y'
'%Y%m%d'
:param str date_str:
:return BusinessDate:
"""
if date_str.count('-'):
str_format = '%Y-%m-%d'
elif date_str.count('.'):
str_format = '%d.%m.%Y'
elif date_str.count('/'):
str_format = '%m/%d/%Y'
else:
str_format = '%Y%m%d'
d = datetime.strptime(date_str, str_format)
return BusinessDate.from_ymd(d.year, d.month, d.day)
# --- cast methods -------------------------------------------------------
[docs] def to_bankdate(self, origin_date):
# deprecated
return self.to_businessdate(origin_date)
[docs] def to_businessdate(self, origin_date=None):
return self
[docs] def to_period(self, origin_date=None):
# deprecated
return self.to_businessperiod(origin_date)
[docs] def to_businessperiod(self, origin_date=None):
if origin_date is None:
origin_date = BusinessDate()
y, m, d = BusinessDate.diff(origin_date, self)
return BusinessPeriod(years=y, months=m, days=d)
[docs] def to_excel(self):
y, m, d = self.to_ymd()
return from_ymd_to_excel(y, m, d)
[docs] def to_ymd(self):
return super(BusinessDate, self).to_ymd(self)
[docs] def to_date(self):
"""
construct datetime.date instance represented calendar date of BusinessDate instance
:return datetime.date:
"""
y, m, d = self.to_ymd()
return date(y, m, d)
[docs] def to_ordinal(self):
return self.to_date().toordinal()
[docs] def to_string(self):
"""
return BusinessDate as 'date.strftime(DATE_FORMAT)'
:return string:
"""
return self.to_date().strftime(DATE_FORMAT)
# --- inherited validation and validation methods ------------------------
@staticmethod
[docs] def is_leap_year(year):
"""
returns True for leap year and False otherwise
:param int year: calendar year
:return bool:
"""
return is_leap_year(year)
@staticmethod
[docs] def days_in_year(year):
"""
returns number of days in the given calendar year
:param int year: calendar year
:return int:
"""
return days_in_year(year)
@staticmethod
[docs] def days_in_month(year, month):
"""
returns number of days for the given year and month
:param int year: calendar year
:param int month: calendar month
:return int:
"""
return days_in_month(year, month)
# --- own validation and information methods -----------------------------
@staticmethod
[docs] def is_date(in_date):
# deprecated
return BusinessDate.is_businessdate(in_date)
@staticmethod
[docs] def is_businessdate(in_date):
"""
checks whether the provided date is a date
:param BusinessDate, int or float in_date:
:return bool:
"""
# Note: if the data range has been created from pace_xl, then all the dates are bank dates
# and here it remains to check the validity.
# !!! However, if the data has been read from json string via json.load() function
# it does not recognize that this numbers are bankdates, just considers them as integers
# therefore, additional check is useful here, first to convert the date if it is integer to BusinessDate,
# then check the validity.
# (as the parameter to this method should always be a BusinessDate)
if not isinstance(in_date, BaseDate):
try: # to be removed
in_date = BusinessDate(in_date)
except:
return False
y, m, d, = in_date.to_ymd()
return is_valid_ymd(y, m, d)
@staticmethod
[docs] def end_of_month(year, month):
return BusinessDate.from_date(date(year, month, BusinessDate.days_in_month(year, month)))
@staticmethod
[docs] def end_of_quarter(year, month):
while not month % 3:
month += 1
return BusinessDate.end_of_month(year, month)
[docs] def is_businessday(self, holiday_obj=None):
"""
:param holiday_obj:
:return:
deprecated method
"""
return self.is_business_day(holiday_obj)
[docs] def is_business_day(self, holiday_obj=None):
"""
:param list holiday_obj : datetime.date list defining businessholidays
:return: bool
method to check if a date falls neither on weekend nor is holiday
"""
if holiday_obj is None:
holiday_obj = DEFAULT_HOLIDAYS
y, m, d = BusinessDate.to_ymd(self)
if weekday(y, m, d) > FRIDAY:
return False
elif self in holiday_obj:
return False
elif date(y, m, d) in holiday_obj:
return False
else:
return True
# --- inherited calculation methods --------------------------------------
[docs] def add_days(self, days):
return BusinessDate(BaseDate.add_days(self, days))
[docs] def add_years(self, years):
return BusinessDate(BaseDate.add_years(self, years))
[docs] def diff_in_days(self, end_bd):
return BaseDate.diff_in_days(self, end_bd)
[docs] def diff_in_years(self, end_bd):
return BaseDate.diff_in_years(self, end_bd)
# --- own calculation methods --------------------------------------------
[docs] def add_period(self, p, holiday_obj=None):
"""
addition of a period object
:param BusinessDate d:
:param p:
:type p: BusinessPeriod or str
:param list holiday_obj:
:return bankdate:
"""
if isinstance(p, (list, tuple)):
return [BusinessDate.add_period(self, pd) for pd in p]
elif isinstance(p, str):
period = BusinessPeriod(p)
else:
period = p
res = BusinessDate.add_years(self, period.years)
res = BusinessDate.add_months(res, period.months)
res = BusinessDate.add_days(res, period.days)
if period.businessdays:
if holiday_obj:
res = BusinessDate.add_business_days(res, period.businessdays, holiday_obj)
else:
res = BusinessDate.add_business_days(res, period.businessdays, period.holiday)
return res
[docs] def add_months(self, month_int):
"""
addition of a number of months
:param BusinessDate d:
:param int month_int:
:return bankdate:
"""
month_int += self.month
while month_int > 12:
self = BusinessDate.add_years(self, 1)
month_int -= 12
while month_int < 1:
self = BusinessDate.add_years(self, -1)
month_int += 12
l = monthrange(self.year, month_int)[1]
return BusinessDate.from_ymd(self.year, month_int, min(l, self.day))
[docs] def add_businessdays(self, days_int, holiday_obj=None):
# deprecated
return BusinessDate.add_business_days(self, days_int, holiday_obj)
[docs] def add_business_days(self, days_int, holiday_obj=None):
"""
private method for the addition of business days, used in the addition of a BusinessPeriod only
:param BusinessDate d:
:param int days_int:
:param list holiday_obj:
:return: BusinessDate
"""
res = self
if days_int >= 0:
count = 0
while count < days_int:
res = BusinessDate.add_days(res, 1)
if BusinessDate.is_business_day(res, holiday_obj):
count += 1
else:
count = 0
while count > days_int:
res = BusinessDate.add_days(res, -1)
if BusinessDate.is_business_day(res, holiday_obj):
count -= 1
return res
[docs] def diff(self, end_date):
"""
difference expressed as a tuple of years, months, days
(see also the python lib dateutils.relativedelta)
:param BusinessDate start_date:
:param BusinessDate end_date:
:return (int, int, int):
"""
if end_date < self:
y, m, d = BusinessDate.diff(end_date, self)
return -y, -m, -d
y = end_date.year - self.year
m = end_date.month - self.month
while m < 0:
y -= 1
m += 12
while m > 12:
y += 1
m -= 12
s = BusinessDate.add_years(BusinessDate.add_months(self, m), y)
d = BusinessDate.diff_in_days(s, end_date)
if d < 0:
m -= 1
if m < 0:
y -= 1
m += 12
s = BusinessDate.add_years(BusinessDate.add_months(self, m), y)
d = BusinessDate.diff_in_days(s, end_date)
return int(y), int(m), int(d)
# --- day count fraction methods -----------------------------------------
[docs] def get_30_360(self, end):
"""
implements 30/360 day count convention
"""
start_day = min(self.day, 30)
end_day = 30 if (start_day == 30 and end.day == 31) else end.day
return (360 * (end.year - self.year) + 30 * (end.month - self.month) + (end_day - start_day)) / 360.0
[docs] def get_act_36525(self, end):
"""
implements Act/365.25 day count convention
"""
return BusinessDate.diff_in_days(self, end) / 365.25
[docs] def get_act_365(self, end):
"""
implements Act/365 day count convention
"""
return BusinessDate.diff_in_days(self, end) / 365.0
[docs] def get_act_360(self, end):
"""
implements Act/360 day count convention
"""
return BusinessDate.diff_in_days(self, end) / 360.0
[docs] def get_act_act(self, end):
"""
implements Act/Act day count convention
"""
# split end-self in year portions
if end.year - self.year == 0:
if BusinessDate.is_leap_year(self.year):
return BusinessDate.diff_in_days(self, end) / 366.0
else:
return BusinessDate.diff_in_days(self, end) / 366.0
elif end.year - self.year == 1:
if BusinessDate.is_leap_year(self.year):
return BusinessDate.diff_in_days(self, BusinessDate.from_date(date(self.year, 12, 31))) / 366.0 + \
BusinessDate.diff_in_days(BusinessDate.from_date(date(self.year, 12, 31)), end) / 365.0
elif BusinessDate.is_leap_year(end.year):
return BusinessDate.diff_in_days(self, BusinessDate.from_date(date(self.year, 12, 31))) / 365.0 + \
BusinessDate.diff_in_days(BusinessDate.from_date(date(self.year, 12, 31)), end) / 366.0
else:
return BusinessDate.diff_in_days(self, end) / 365.0
else:
raise NotImplementedError('Act/Act day count not implemented for periods spanning three years or more.')
# --- business day adjustment methods ------------------------------------
[docs] def adjust_previous(self, holidays_obj=None):
while not BusinessDate.is_businessday(self, holidays_obj):
self = BusinessDate.add_days(self, -1)
return self
[docs] def adjust_follow(self, holidays_obj=None):
while not BusinessDate.is_businessday(self, holidays_obj):
self = BusinessDate.add_days(self, 1)
return self
[docs] def adjust_mod_follow(self, holidays_obj=None):
month = self.month
new = BusinessDate.adjust_follow(self, holidays_obj)
if month != new.month:
new = BusinessDate.adjust_previous(self, holidays_obj)
self = new
return self
[docs] def adjust_mod_previous(self, holidays_obj=None):
month = self.month
new = BusinessDate.adjust_previous(self, holidays_obj)
if month != new.month:
new = BusinessDate.adjust_follow(self, holidays_obj)
self = new
return self
[docs] def adjust_start_of_month(self, holidays_obj=None):
self = BusinessDate.from_date(date(self.year, self.month, 1))
self = self.adjust_follow(holidays_obj)
return self
[docs] def adjust_end_of_month(self, holidays_obj=None):
self = BusinessDate.end_of_month(self.year, self.month)
self = self.adjust_previous(holidays_obj)
return self
[docs] def adjust_imm(self, holidays_obj=None):
self = BusinessDate.end_of_quarter(self.year, self.month)
self = BusinessDate.from_date(date(self.year, self.month, 15))
while weekday(self.year, self.month, self.day) == WEDNESDAY:
BusinessDate.add_days(self, 1)
return self
[docs] def adjust_cds_imm(self, holidays_obj=None):
eoq = BusinessDate.end_of_quarter(self.year, self.month)
self = BusinessDate.from_date(date(eoq.year, eoq.month, 20))
return self
[docs]class BusinessPeriod(BasePeriod):
def __new__(cls, *args, **kwargs):
new = super(BusinessPeriod, cls).__new__(cls)
return new
def __init__(self, period_in='', holiday=None, years=0, months=0, days=0, businessdays=0):
"""
class managing date periods like days, weeks, years etc.
:param period_in:
:param holiday:
:param years:
:param months:
:param days:
:param businessdays:
representation of a time BusinessPeriod, similar to dateutils.relativedelta, but with additional business day logic
"""
super(BusinessPeriod, self).__init__()
if isinstance(period_in, BusinessPeriod):
years = period_in.years
months = period_in.months
days = period_in.days
businessdays = period_in.businessdays
elif isinstance(period_in, timedelta):
days += timedelta.days
elif isinstance(period_in, (list, tuple)):
if len(period_in) == 2:
y, m, d = BusinessDate.diff(period_in[0], period_in[1])
years += y
months += m
days += d
elif isinstance(period_in, str):
if period_in.startswith('-'):
p = BusinessPeriod(period_in[1:])
years -= p.years
months -= p.months
days -= p.days
businessdays -= p.businessdays
elif period_in.upper() == 'ON':
businessdays += 1
elif period_in.upper() == 'TN':
businessdays += 2
elif period_in.upper() == 'DD':
businessdays += 3
elif period_in.upper().find('B') > 0:
businessdays += int(period_in.upper().split('B', 2)[0])
else:
y, m, d = BusinessPeriod.parse(period_in)
years += y
months += m
days += d
if holiday is None:
self.holiday = DEFAULT_HOLIDAYS
else:
self.holiday = holiday
self.years = years
self.months = months
self.days = days
self.businessdays = businessdays
# --- constructor methods ------------------------------------------------
@classmethod
[docs] def from_string(cls, period_in):
return BusinessPeriod(period_in)
# --- validation and information methods ---------------------------------
@classmethod
[docs] def parse(cls, period_str):
p = period_str.upper()
Y, Q, M, W, D = '00000'
if p.find('Y') > 0:
[Y, p] = p.split('Y', 2)
if p.find('Q') > 0:
[Q, p] = p.split('Q', 2)
if p.find('M') > 0:
[M, p] = p.split('M', 2)
if p.find('W') > 0:
[W, p] = p.split('W', 2)
if p.find('D') > 0:
[D, p] = p.split('D', 2)
assert Y.isdigit() and Q.isdigit() and M.isdigit() and W.isdigit() and D.isdigit()
y = int(Y)
m = int(Q) * 3 + int(M)
d = int(W) * 7 + int(D)
return (y, m, d)
@classmethod
[docs] def is_period(cls, in_period):
# deprecated
return cls.is_businessperiod(in_period)
@classmethod
[docs] def is_businessperiod(cls, in_period):
"""
:param in_period: object to be checked
:type in_period: object, str, timedelta
:return: True if cast works
:rtype: Boolean
checks is argument con becasted to BusinessPeriod
"""
try: # to be removed
if in_period.upper() == '0D':
return True
else:
p = BusinessPeriod(str(in_period))
return not (p.days == 0 and p.months == 0 and p.years == 0 and p.businessdays == 0)
except:
return False
# --- property methods ---------------------------------------------------
pass
# --- operator methods ---------------------------------------------------
def __repr__(self):
return self.to_string()
def __str__(self):
# return self.__class__.__name__ + '(' + self.to_string() + ')'
return self.to_string()
def __abs__(self):
self.years = abs(self.years)
self.months = abs(self.months)
self.days = abs(self.days)
self.businessdays = abs(self.businessdays)
def __cmp__(self, other):
assert type(self) == type(other), "types don't match %s" % str((type(self), type(other)))
d = BusinessDate()
return BusinessDate.diff_in_days(BusinessDate.add_period(d, self), BusinessDate.add_period(d, other))
def __eq__(self, other):
if isinstance(other, type(self)):
return 0.0 == self.__cmp__(other)
else:
return False
def __ne__(self, other):
return not self.__eq__(other)
def __nonzero__(self):
return True if self.years or self.months or self.days or self.businessdays else False
def __add__(self, other):
if isinstance(other, (list, tuple)):
return [self.__add__(o) for o in other]
elif isinstance(other, BusinessPeriod):
return BusinessPeriod(self).add_businessperiod(other)
elif BusinessPeriod.is_period(other):
return self + BusinessPeriod(other)
else:
raise TypeError
def __sub__(self, other):
per = self.__rsub__(other)
return BusinessPeriod(years=-per.years, months=-per.months, days=-per.days, businessdays=-per.businessdays)
def __rsub__(self, other):
if isinstance(other, (list, tuple)):
return [self.__rsub__(o) for o in other]
elif BusinessPeriod.is_period(other):
p = BusinessPeriod(other)
y = self.years + p.years
m = self.months + p.months
d = self.days + p.days
b = self.businessdays + p.businessdays
other = BusinessPeriod(years=y, months=m, days=d, businessdays=b)
else:
raise TypeError
return other
def __mul__(self, other):
if isinstance(other, (list, tuple)):
return [self.__mul__(o) for o in other]
if not isinstance(other, (int, long)):
pass
assert isinstance(other, (int, long)), "expected int or long but got %s" % str(type(other))
y = self.years * other
m = self.months * other
d = self.days * other
b = self.businessdays * other
return BusinessPeriod(years=y, months=m, days=d, businessdays=b)
def __rmul__(self, other):
return self.__mul__(other)
# --- calculation methods ------------------------------------------------
[docs] def add_years(self, years_int):
self.years += years_int
return self
[docs] def add_months(self, months_int):
self.months += months_int
return self
[docs] def add_days(self, days_int):
self.days += days_int
return self
[docs] def add_businessdays(self, days_int):
self.businessdays += days_int
return self
[docs] def add_businessperiod(self, p):
self.years += p.years
self.months += p.months
self.days += p.days
self.businessdays += p.businessdays
return self
# --- cast methods -------------------------------------------------------
[docs] def to_date(self, start_date=None):
return self.to_bankdate(start_date).to_date()
[docs] def to_datetime(self, start_date=None):
return self.to_bankdate(start_date).to_datetime()
[docs] def to_bankdate(self, start_date=None):
# deprecated
return self.to_businessdate(start_date)
[docs] def to_businessdate(self, start_date=None):
if start_date:
return BusinessDate.add_period(start_date, self)
else:
return BusinessDate.add_period(BusinessDate(), self)
[docs] def to_period(self, start_date=None):
# deprecated
return self.to_businessperiod(start_date)
[docs] def to_businessperiod(self, start_date=None):
return self
[docs] def to_string(self):
period_str = ''
if self.years:
period_str += str(self.years) + 'Y'
if self.months:
period_str += str(self.months) + 'M'
if self.days:
period_str += str(self.days) + 'D'
if self.businessdays:
period_str += str(self.businessdays) + 'B'
if not period_str:
period_str += '0D'
return period_str
[docs]class BusinessRange(list):
def __init__(self, start, stop=None, step=None, rolling=None):
"""
range like class to build date list
:param start: date to begin schedule, if stop not given, start will be used as stop and
default in rolling to BusinessDate()
:type start: BusinessDate or int or str
:param stop: date to stop before, if not given, start will be used for stop instead
:type stop: BusinessDate or int or str
:param step: period to step schedule, if not given 1 year is default
:type step: BusinessPeriod or str
:param rolling: date to roll on (forward and backward) between start and stop,
if not given default will be start
:type rolling: BusinessDate or int or str
range like class to build BusinessDate schedule from rolling date and BusinessPeriod
"""
if stop is None:
stop = start
start = BusinessDate()
if step is None:
step = BusinessPeriod(years=1)
if rolling is None:
rolling = start
# make proper businessdate objects
if not isinstance(start, BusinessDate):
if isinstance(start, BusinessPeriod):
start = start.to_businessdate()
else:
start = BusinessDate(start)
if not isinstance(rolling, BusinessDate):
if isinstance(rolling, BusinessPeriod):
rolling = rolling.to_businessdate()
else:
rolling = BusinessDate(rolling)
if not isinstance(stop, BusinessDate):
if isinstance(stop, BusinessPeriod):
stop = stop.to_businessdate()
else:
stop = BusinessDate(stop)
if not isinstance(step, BusinessPeriod):
if isinstance(step, BusinessDate):
step = step.to_businessperiod()
else:
step = BusinessPeriod(step)
# roll schedule
current = BusinessDate(rolling)
schedule = [BusinessDate(rolling)]
# roll backward
current = BusinessDate.add_period(current, step)
while current < start:
current = BusinessDate.add_period(current, step)
while current < stop:
schedule.append(current)
current = BusinessDate.add_period(current, step)
# roll forward
back_step = -1 * BusinessPeriod(step)
current = BusinessDate(rolling)
current = BusinessDate.add_period(current, back_step)
while stop <= current:
current = BusinessDate.add_period(current, back_step)
while start <= current:
schedule.append(current)
current = BusinessDate.add_period(current, back_step)
# remove rolldate if rolldate is enddate or later
if not start <= rolling < stop:
schedule.remove(rolling)
# push to super
super(BusinessRange, self).__init__(set(schedule))
self.sort()
[docs] def adjust(self, convention=None, holidays_obj=None):
if convention is None:
convention = 'mod_follow'
adj_func = getattr(BusinessDate, 'adjust_' + convention.lower())
adj_list = [adj_func(d, holidays_obj) for d in self]
del self[:]
super(BusinessRange, self).extend(adj_list)
return self
[docs] def adjust_previous(self, holidays_obj=None):
return self.adjust('previous', holidays_obj)
[docs] def adjust_follow(self, holidays_obj=None):
return self.adjust('follow', holidays_obj)
[docs] def adjust_mod_previous(self, holidays_obj=None):
return self.adjust('mod_previous', holidays_obj)
[docs] def adjust_mod_follow(self, holidays_obj=None):
return self.adjust('mod_follow', holidays_obj)
[docs] def adjust_start_of_month(self, holidays_obj=None):
return self.adjust('start_of_month', holidays_obj)
[docs] def adjust_end_of_month(self, holidays_obj=None):
return self.adjust('end_of_month', holidays_obj)
[docs] def adjust_imm(self, holidays_obj=None):
return self.adjust('imm', holidays_obj)
[docs] def adjust_cds_imm(self, holidays_obj=None):
return self.adjust('cds_imm', holidays_obj)
[docs]class BusinessSchedule(BusinessRange):
def __init__(self, start, end, step, roll=None):
"""
class to build date schedules incl. start and end date
:param BusinessDate start: start date of schedule
:param BusinessDate end: end date of schedule
:param BusinessPeriod step: period distance of two dates
:param BusinessDate roll: origin of schedule
convenient class to build date schedules
a schedule includes always start and end date
and rolls on roll, i.e. builds a sequence by
adding and/or substracting step to/from roll.
start and end slice the relevant dates.
"""
if not roll:
roll = end
super(BusinessSchedule, self).__init__(start, end, step, roll)
if not isinstance(start, BusinessDate):
if isinstance(start, BusinessPeriod):
start = start.to_businessdate()
else:
start = BusinessDate(start)
if not isinstance(end, BusinessDate):
if isinstance(end, BusinessPeriod):
end = end.to_businessdate()
else:
end = BusinessDate(end)
if start not in self:
self.insert(0, start)
if end not in self:
self.append(end)
[docs] def first_stub_long(self):
if len(self)>2:
self.pop(1)
return self
[docs] def last_stub_long(self):
if len(self)>2:
self.pop(-2)
return self
[docs]def easter(year):
"""
This method was ported from the work done by GM Arts,
on top of the algorithm by Claus Tondering, which was
based in part on the algorithm of Ouding (1940), as
quoted in "Explanatory Supplement to the Astronomical
Almanac", P. Kenneth Seidelmann, editor.
More about the algorithm may be found at:
http://users.chariot.net.au/~gmarts/eastalg.htm
and
http://www.tondering.dk/claus/calendar.html
"""
# g - Golden year - 1
# c - Century
# h - (23 - Epact) mod 30
# i - Number of days from March 21 to Paschal Full Moon
# j - Weekday for PFM (0=Sunday, etc)
# p - Number of days from March 21 to Sunday on or before PFM
# (-6 to 28 methods 1 & 3, to 56 for method 2)
# e - Extra days to add for method 2 (converting Julian
# date to Gregorian date)
y = year
g = y % 19
e = 0
c = y // 100
h = (c - c // 4 - (8 * c + 13) // 25 + 19 * g + 15) % 30
i = h - (h // 28) * (1 - (h // 28) * (29 // (h + 1)) * ((21 - g) // 11))
j = (y + y // 4 + i + 2 - c + c // 4) % 7
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
# (later dates apply to method 2, although 23 May never actually occurs)
p = i - j + e
d = 1 + (p + 27 + (p + 6) // 40) % 31
m = 3 + (p + 26) // 30
return date(int(y), int(m), int(d))
[docs]def target_days(year):
self = dict()
self[date(year, 1, 1)] = "New Year's Day"
e = easter(year)
self[e + timedelta(-2)] = "Black Friday"
self[e + timedelta(1)] = "Easter Monday"
self[date(year, 5, 1)] = "Labour Day"
self[date(year, 12, 25)] = "First Christmas Day"
self[date(year, 12, 26)] = "Second Christmas Day"
return self