12

I'm trying to create a generator function to iterate over business days (weekdays), skipping weekends (and holidays would be nice too!). So far, I only have a function that simply iterates over days:

def daterange(startDate, endDate):
    for i in xrange(int((endDate - startDate).days)):
        yield startDate + timedelta(i)

I'm struggling to figure out a clean, efficient, and pythonic way to make the generator skip over weekends and holidays. Thanks in advance!

jh314
  • 27,144
  • 16
  • 62
  • 82
  • 1
    See this question for the holidays: http://stackoverflow.com/questions/1986207/holiday-calendars-file-formats-et-al – Mark Ransom Jul 18 '12 at 21:30

5 Answers5

27

I would strong recommend using the dateutil library for such tasks. A basic (not ignoring holidays) iterator over business days then simply is:

from dateutil.rrule import DAILY, rrule, MO, TU, WE, TH, FR

def daterange(start_date, end_date):
  return rrule(DAILY, dtstart=start_date, until=end_date, byweekday=(MO,TU,WE,TH,FR))
earl
  • 40,327
  • 6
  • 58
  • 59
  • Does this actually do what you say? I tried it on Python 2.7 and 3.3 on Linux and Mac OS, and in all cases it returns all days, including weekends. If you look at `dateutil.rrule.WDAYMASK` you may see that it is a list of 0-6, i.e. all days, not only Monday-Friday. – John Zwinck Jun 25 '13 at 10:21
  • 1
    @JohnZwinck Right, WDAYMASK is indeed incorrect (at least with current versions of dateutil). I updated the answer to reflect this. – earl Jun 25 '13 at 22:06
  • use `from dateutil.rrule import *` else you will get error as MO not identified – adam Sep 12 '13 at 08:12
  • 1
    dateutil does not support iterating backwards, just in case you have a use case for this. Apparently the author doesn't think this important. https://bugs.launchpad.net/dateutil/+bug/903925 – Thomas Farvour Jul 07 '14 at 18:05
12

Assuming startDate and endDate are datetime or date objects, you can use the weekday method to get the day of the week, then skip it if it's Saturday or Sunday. Just do:

def daterange(startDate, endDate):
    for i in xrange(int((endDate - startDate).days)):
        nextDate = startDate + timedelta(i)
        if nextDate.weekday() not in (5, 6):
            yield startDate + timedelta(i)

For holidays you will have to check manually for each holiday you want. Some holidays are defined in complex ways so this could be a bit tricky.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
11

Python pandas has a built-in method bdate_range() with Business Days as it's default frequency. https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.bdate_range.html

import pandas as pd
pd.bdate_range(start='1/25/2020', end='2/24/2020')
SGI
  • 303
  • 3
  • 10
7

There's a useful library called dateutil that can do this sort of thing for you. It can generate ranges of dates (or dates based on custom rules), excluding certain days, consider a week starting on a day etc... Also has a somewhat more flexible timedelta than the builtin datetime library.

Docs at http://labix.org/python-dateutil/ - and available on PyPi

Jon Clements
  • 138,671
  • 33
  • 247
  • 280
1
def get_date_range(start, end, workdays=None, holidays=None, skip_non_workdays=True):
"""
This function calculates the durations between 2 dates skipping non workdays and holidays as specified
:rtype : tuple
:param start: date
:param end: date
:param workdays: string [Comma Separated Values, 0 - Monday through to 6 - Sunday e.g "0,1,2,3,4"]
:param holidays: list
:param skip_non_workdays: boolean
:return:
"""
from datetime import timedelta

duration = 0

# define workdays
if workdays is None:
    workdays = [0, 1, 2, 3, 4]
else:
    workdays = workdays.split(",")

# check if we need to skip non workdays
if skip_non_workdays is False:
    workdays = [0, 1, 2, 3, 4, 5, 6]

# validate dates
if end < start:
    return False, "End date is before start date"

# now its time for us to iterate
i = start
while i <= end:

    # first let's give benefit of the doubt
    incr = True

    # lets see if day is in the workday array if not then fault it's existence here
    try:
        workdays.index(i.weekday())
    except ValueError:
        incr = False

    # lets check if day is an holiday, charge guilty if so.
    # We are checking the index in holiday array
    try:
        holidays.index(i)
        incr = False
    except (ValueError, AttributeError):
        pass

    if incr:
        duration += 1
        print "This day passed the criterion %s" % i

    i += timedelta(1)

return True, duration