I’m trying to add n (integer) working days to a given date, the date addition has to avoid the holidays and weekends (it’s not included in the working days)
Skipping weekends would be pretty easy doing something like this:
import datetime def date_by_adding_business_days(from_date, add_days): business_days_to_add = add_days current_date = from_date while business_days_to_add > 0: current_date += datetime.timedelta(days=1) weekday = current_date.weekday() if weekday >= 5: # sunday = 6 continue business_days_to_add -= 1 return current_date #demo: print '10 business days from today:' print date_by_adding_business_days(datetime.date.today(), 10)
The problem with holidays is that they vary a lot by country or even by region, religion, etc. You would need a list/set of holidays for your use case and then skip them in a similar way. A starting point may be the calendar feed that Apple publishes for iCal (in the ics format), the one for the US would be http://files.apple.com/calendars/US32Holidays.ics
You could use the icalendar module to parse this.
If you don’t mind using a 3rd party library then dateutil is handy
from dateutil.rrule import * print "In 4 business days, it's", rrule(DAILY, byweekday=(MO,TU,WE,TH,FR))
You can also look at
rruleset and using
.exdate() to provide the holidays to skip those in the calculation, and optionally there’s a
cache option to avoid re-calculating that might be worth looking in to.
There is no real shortcut to do this. Try this approach:
- Create a class which has a method
skip(self, d)which returns
Truefor dates that should be skipped.
- Create a dictionary in the class which contains all holidays as date objects. Don’t use
datetimeor similar because the fractions of a day will kill you.
Truefor any date that is in the dictionary or
d.weekday() >= 5
To add N days, use this method:
def advance(d, days): delta = datetime.timedelta(1) for x in range(days): d = d + delta while holidayHelper.skip(d): d = d + delta return d
Thanks based on omz code i made some little changes …it maybe helpful for other users:
import datetime def date_by_adding_business_days(from_date, add_days,holidays): business_days_to_add = add_days current_date = from_date while business_days_to_add > 0: current_date += datetime.timedelta(days=1) weekday = current_date.weekday() if weekday >= 5: # sunday = 6 continue if current_date in holidays: continue business_days_to_add -= 1 return current_date #demo: Holidays =[datetime.datetime(2012,10,3),datetime.datetime(2012,10,4)] print date_by_adding_business_days(datetime.datetime(2012,10,2), 10,Holidays)
I wanted a solution that wasn’t O(N) and it looked like a fun bit of code golf. Here’s what I banged out in case anyone’s interested. Works for positive and negative numbers. Let me know if I missed anything.
def add_business_days(d, business_days_to_add): num_whole_weeks = business_days_to_add / 5 extra_days = num_whole_weeks * 2 first_weekday = d.weekday() remainder_days = business_days_to_add % 5 natural_day = first_weekday + remainder_days if natural_day > 4: if first_weekday == 5: extra_days += 1 elif first_weekday != 6: extra_days += 2 return d + timedelta(business_days_to_add + extra_days)
This will take some work since there isn’t any defined construct for holidays in any library (by my knowledge at least). You will need to create your own enumeration of those.
Checking for weekend days is done easily by calling
.weekday() < 6 on your datetime object.
Hope this helps. It’s not
O(holidays). Also, holidays only works when the offset is positive.
def add_working_days(start, working_days, holidays=()): """ Add working_days to start start date , skipping weekends and holidays. :param start: the date to start from :type start: datetime.datetime|datetime.date :param working_days: offset in working days you want to add (can be negative) :type working_days: int :param holidays: iterator of datetime.datetime of datetime.date instances :type holidays: iter(datetime.date|datetime.datetime) :return: the new date wroking_days date from now :rtype: datetime.datetime :raise: ValueError if working_days < 0 and holidays """ assert isinstance(start, (datetime.date, datetime.datetime)), 'start should be a datetime instance' assert isinstance(working_days, int) if working_days < 0 and holidays: raise ValueError('Holidays and a negative offset is not implemented. ') if working_days == 0: return start # first just add the days new_date = start + datetime.timedelta(working_days) # now compensate for the weekends. # the days is 2 times plus the amount of weeks are included in the offset added to the day of the week # from the start. This compensates for adding 1 to a friday because 4+1 // 5 = 1 new_date += datetime.timedelta(2 * ((working_days + start.weekday()) // 5)) # now compensate for the holidays # process only the relevant dates so order the list and abort the handling when the holiday is no longer # relevant. Check each holiday not being in a weekend, otherwise we don't mind because we skip them anyway # next, if a holiday is found, just add 1 to the date, using the add_working_days function to compensate for # weekends. Don't pass the holiday to avoid recursion more then 1 call deep. for hday in sorted(holidays): if hday < start: # ignore holidays before start, we don't care continue if hday.weekday() > 4: # skip holidays in weekends continue if hday <= new_date: # only work with holidays up to and including the current new_date. # increment using recursion to compensate for weekends new_date = add_working_days(new_date, 1) else: break return new_date
If someone needs to add/substract days, extending @omz’s answer:
def add_business_days(from_date, ndays): business_days_to_add = abs(ndays) current_date = from_date sign = ndays/abs(ndays) while business_days_to_add > 0: current_date += datetime.timedelta(sign * 1) weekday = current_date.weekday() if weekday >= 5: # sunday = 6 continue business_days_to_add -= 1 return current_date