Number of days between 2 dates, excluding weekends

Viewed 109084

How can I calculate number of days between two dates excluding weekends?

21 Answers

So far, I found none of the provided solutions satisfactory. Either there is a dependency to a lib I don't want or there are inefficient looping algorithms or there are algorithms that won't work for all cases. Unfortunately the one provided by @neil did not work sufficiently well. This was corrected by @vekerdyb's answer which unfortunately did not work for all cases, either (pick a Saturday or a Sunday on the same weekend for example...).
So I sat down and tried my best to come up with a solution that is working for all dates entered. It's small and efficient. Feel free to find errors in this one, as well, of course. Beginning and end are inclusive (so Monday-Tuesday in one week are 2 workdays for example).

def get_workdays(from_date: datetime, to_date: datetime):
    # if the start date is on a weekend, forward the date to next Monday
    if from_date.weekday() > 4:
        from_date = from_date + timedelta(days=7 - from_date.weekday())
    # if the end date is on a weekend, rewind the date to the previous Friday
    if to_date.weekday() > 4:
        to_date = to_date - timedelta(days=to_date.weekday() - 4)
    if from_date > to_date:
        return 0
    # that makes the difference easy, no remainders etc
    diff_days = (to_date - from_date).days + 1
    weeks = int(diff_days / 7)
    return weeks * 5 + (to_date.weekday() - from_date.weekday()) + 1

Here's something I use for my management scripts, which takes into account holidays, regardless of which country you're in (uses a web service to pull in country-specific holiday data). Needs a bit of efficiency refactoring but besides that, it works.

from dateutil import rrule
from datetime import datetime
import pytz

timezone_manila = pytz.timezone('Asia/Manila')

class Holidays(object):
    
    def __init__(self, holidaydata):
        self.holidaydata = holidaydata
        
    def isHoliday(self,dateobj):
        for holiday in self.holidaydata:
            d = datetime(holiday['date']['year'], holiday['date']['month'], holiday['date']['day'], tzinfo=timezone_manila)            
            if d == dateobj:
                return True
        
        return False

def pullHolidays(start, end):
    import urllib.request, json
    urlstring = "https://kayaposoft.com/enrico/json/v2.0/?action=getHolidaysForDateRange&fromDate=%s&toDate=%s&country=phl&region=dc&holidayType=public_holiday" % (start.strftime("%d-%m-%Y"),end.strftime("%d-%m-%Y")) 
    
    with urllib.request.urlopen(urlstring) as url:
        holidaydata = json.loads(url.read().decode())
    
    return Holidays(holidaydata)


def countWorkDays(start, end):
    workdays=0
    holidayData=pullHolidays(start,end)
    for dt in rrule.rrule(rrule.DAILY, dtstart=start, until=end):
        if dt.weekday() < 5:
            if holidayData.isHoliday(dt) == False:
                workdays+=1
    
    return workdays

1. With using external service to get public holidays/extra work days

Can't thank @Renan enough for introducing this amazing API that I'm now using in my own project. Here's his answer with slight cleaning + tests.

import urllib.request
import json
from typing import Dict

from dateutil import rrule
from datetime import date


WEEKDAY_FRIDAY = 4  # date.weekday() starts with 0


class CountryCalendar(object):

    def __init__(self, special_dates: Dict[date, str]):
        self.special_dates = special_dates

    def is_working_day(self, dt: date):
        date_type = self.special_dates.get(dt)
        if date_type == "extra_working_day":
            return True
        if date_type == "public_holiday":
            return False
        return dt.weekday() <= WEEKDAY_FRIDAY


def load_calendar(
    country: str,
    region: str,
    start_date: date,
    end_date: date
) -> CountryCalendar:
    """
    Access Enrico Service 2.0 JSON
    https://kayaposoft.com/enrico/

    Response format (for country=rus):

    [
        {
            holidayType: "public_holiday",
            date: {
                day: 2,
                month: 1,
                year: 2022,
                dayOfWeek: 7
            },
            name: [
                {lang: "ru", text: "Новогодние каникулы"},
                {lang: "en", text: "New Year’s Holiday"}
            ]
        },
        ...
    ]
    """
    urlstring = (
        f"https://kayaposoft.com/enrico/json/v2.0/"
        f"?action=getHolidaysForDateRange"
        f"&fromDate={start_date:%d-%m-%Y}"
        f"&toDate={end_date:%d-%m-%Y}"
        f"&country={country}"
        f"&region={region}"
        f"&holidayType=all"
    )

    with urllib.request.urlopen(urlstring) as url:
        payload = json.loads(url.read().decode())

    return CountryCalendar({
        date(
            special_date["date"]["year"],
            special_date["date"]["month"],
            special_date["date"]["day"],
        ): special_date["holidayType"]
        for special_date in payload
    })


def count_work_days(
    start_date: date,
    end_date: date,
    country: str,
    region: str,
):
    """
    Get working days specific for country
    using public holidays internet provider
    """
    if start_date > end_date:
        return 0

    try:
        country_calendar = load_calendar(country, region, start_date, end_date)
    except Exception as exc:
        print(f"Error accessing calendar of country: {country}. Exception: {exc}")
        raise

    workdays = 0
    for dt in rrule.rrule(rrule.DAILY, dtstart=start_date, until=end_date):
        if country_calendar.is_working_day(dt):
            workdays += 1

    return workdays


def test(test_name: str, start_date: date, end_date: date, expected: int):
    print(f"Running: {test_name}... ", end="")
    params = dict(
        start_date=start_date,
        end_date=end_date,
        country="rus",
        region=""
    )
    assert expected == count_work_days(**params), dict(
        expected=expected,
        actual=count_work_days(**params),
        **params
    )
    print("ok")


# Start on Mon
test("Mon - Mon", date(2022, 4, 4), date(2022, 4, 4), 1)
test("Mon - Tue", date(2022, 4, 4), date(2022, 4, 5), 2)
test("Mon - Wed", date(2022, 4, 4), date(2022, 4, 6), 3)
test("Mon - Thu", date(2022, 4, 4), date(2022, 4, 7), 4)
test("Mon - Fri", date(2022, 4, 4), date(2022, 4, 8), 5)
test("Mon - Sut", date(2022, 4, 4), date(2022, 4, 9), 5)
test("Mon - Sun", date(2022, 4, 4), date(2022, 4, 10), 5)
test("Mon - next Mon", date(2022, 4, 4), date(2022, 4, 11), 6)
test("Mon - next Tue", date(2022, 4, 4), date(2022, 4, 12), 7)


# Start on Fri
test("Fri - Sut", date(2022, 4, 1), date(2022, 4, 2), 1)
test("Fri - Sun", date(2022, 4, 1), date(2022, 4, 3), 1)
test("Fri - Mon", date(2022, 4, 1), date(2022, 4, 4), 2)
test("Fri - Tue", date(2022, 4, 1), date(2022, 4, 5), 3)
test("Fri - Wed", date(2022, 4, 1), date(2022, 4, 6), 4)
test("Fri - Thu", date(2022, 4, 1), date(2022, 4, 7), 5)
test("Fri - next Fri", date(2022, 4, 1), date(2022, 4, 8), 6)
test("Fri - next Sut", date(2022, 4, 1), date(2022, 4, 9), 6)
test("Fri - next Sun", date(2022, 4, 1), date(2022, 4, 10), 6)
test("Fri - next Mon", date(2022, 4, 1), date(2022, 4, 11), 7)


# Some edge cases
test("start > end", date(2022, 4, 2), date(2022, 4, 1), 0)
test("Sut - Sun", date(2022, 4, 2), date(2022, 4, 3), 0)
test("Sut - Mon", date(2022, 4, 2), date(2022, 4, 4), 1)
test("Sut - Fri", date(2022, 4, 2), date(2022, 4, 8), 5)
test("Thu - Fri", date(2022, 3, 31), date(2022, 4, 8), 7)

2. Simple math: without usage of service for public holidays/extra work days

Even if @Sebastian answer can't be applied in many cases, as it's not considering public holidays and extra working days, I still find it great, as it's do the job without and decided to fix a bug (basically only his last line was changed).

from datetime import date, timedelta


WEEKDAY_FRIDAY = 4  # date.weekday() starts with 0


def count_work_days(start_date: date, end_date: date):
    """
    Math function to get workdays between 2 dates.
    Can be used only as fallback as it doesn't know
    about specific country holidays or extra working days.
    """
    # if the start date is on a weekend, forward the date to next Monday

    if start_date.weekday() > WEEKDAY_FRIDAY:
        start_date = start_date + timedelta(days=7 - start_date.weekday())

    # if the end date is on a weekend, rewind the date to the previous Friday
    if end_date.weekday() > WEEKDAY_FRIDAY:
        end_date = end_date - timedelta(days=end_date.weekday() - WEEKDAY_FRIDAY)

    if start_date > end_date:
        return 0
    # that makes the difference easy, no remainders etc
    diff_days = (end_date - start_date).days + 1
    weeks = int(diff_days / 7)

    remainder = end_date.weekday() - start_date.weekday() + 1

    if remainder != 0 and end_date.weekday() < start_date.weekday():
        remainder = 5 + remainder

    return weeks * 5 + remainder


def test(test_name: str, start_date: date, end_date: date, expected: int):
    print(f"Running: {test_name}... ", end="")
    params = dict(
        start_date=start_date,
        end_date=end_date,
    )
    assert expected == count_work_days(**params), dict(
        expected=expected,
        actual=count_work_days(**params),
        **params
    )
    print("ok")


# Start on Mon
test("Mon - Mon", date(2022, 4, 4), date(2022, 4, 4), 1)
test("Mon - Tue", date(2022, 4, 4), date(2022, 4, 5), 2)
test("Mon - Wed", date(2022, 4, 4), date(2022, 4, 6), 3)
test("Mon - Thu", date(2022, 4, 4), date(2022, 4, 7), 4)
test("Mon - Fri", date(2022, 4, 4), date(2022, 4, 8), 5)
test("Mon - Sut", date(2022, 4, 4), date(2022, 4, 9), 5)
test("Mon - Sun", date(2022, 4, 4), date(2022, 4, 10), 5)
test("Mon - next Mon", date(2022, 4, 4), date(2022, 4, 11), 6)
test("Mon - next Tue", date(2022, 4, 4), date(2022, 4, 12), 7)


# Start on Fri
test("Fri - Sut", date(2022, 4, 1), date(2022, 4, 2), 1)
test("Fri - Sun", date(2022, 4, 1), date(2022, 4, 3), 1)
test("Fri - Mon", date(2022, 4, 1), date(2022, 4, 4), 2)
test("Fri - Tue", date(2022, 4, 1), date(2022, 4, 5), 3)
test("Fri - Wed", date(2022, 4, 1), date(2022, 4, 6), 4)
test("Fri - Thu", date(2022, 4, 1), date(2022, 4, 7), 5)
test("Fri - next Fri", date(2022, 4, 1), date(2022, 4, 8), 6)
test("Fri - next Sut", date(2022, 4, 1), date(2022, 4, 9), 6)
test("Fri - next Sun", date(2022, 4, 1), date(2022, 4, 10), 6)
test("Fri - next Mon", date(2022, 4, 1), date(2022, 4, 11), 7)


# Some edge cases
test("start > end", date(2022, 4, 2), date(2022, 4, 1), 0)
test("Sut - Sun", date(2022, 4, 2), date(2022, 4, 3), 0)
test("Sut - Mon", date(2022, 4, 2), date(2022, 4, 4), 1)
test("Sut - Fri", date(2022, 4, 2), date(2022, 4, 8), 5)
test("Thu - Fri", date(2022, 3, 31), date(2022, 4, 8), 7)

You can use the following foolproof function to get the number of working days between any two given dates:

import datetime

def working_days(start_dt,end_dt):
    num_days = (end_dt -start_dt).days +1
    num_weeks =(num_days)//7
    a=0
    #condition 1
    if end_dt.strftime('%a')=='Sat':
        if start_dt.strftime('%a') != 'Sun':
            a= 1
    #condition 2
    if start_dt.strftime('%a')=='Sun':
        if end_dt.strftime('%a') !='Sat':
            a =1
    #condition 3
    if end_dt.strftime('%a')=='Sun':
        if start_dt.strftime('%a') not in ('Mon','Sun'):
            a =2
    #condition 4        
    if start_dt.weekday() not in (0,6):
        if (start_dt.weekday() -end_dt.weekday()) >=2:
            a =2
    working_days =num_days -(num_weeks*2)-a

    return working_days

Usage example:

start_dt = datetime.date(2019,6,5)
end_dt = datetime.date(2019,6,21)

working_days(start_dt,end_dt)

Here, both the start date and the end date is included, excluding all the weekends.
Hope this helps!!

you may use https://pypi.org/project/python-networkdays/ The package has no dependencies, no NumPy or pandas to calculate a date. ;)

In [3]: import datetime

In [4]: from networkdays import networkdays

In [5]: HOLIDAYS  = { datetime.date(2020, 12, 25),}

In [6]: days = networkdays.Networkdays(datetime.date(2020, 12, 1),datetime.date(2020, 12, 31), holidays=HOLIDAYS, weekdaysoff={6,7})

In [7]: days.networkdays()
Out[7]:
[datetime.date(2020, 12, 1),
 datetime.date(2020, 12, 2),
 datetime.date(2020, 12, 3),
 datetime.date(2020, 12, 4),
 datetime.date(2020, 12, 7),
 datetime.date(2020, 12, 8),
 datetime.date(2020, 12, 9),
 datetime.date(2020, 12, 10),
 datetime.date(2020, 12, 11),
 datetime.date(2020, 12, 14),
 datetime.date(2020, 12, 15),
 datetime.date(2020, 12, 16),
 datetime.date(2020, 12, 17),
 datetime.date(2020, 12, 18),
 datetime.date(2020, 12, 21),
 datetime.date(2020, 12, 22),
 datetime.date(2020, 12, 23),
 datetime.date(2020, 12, 24),
 datetime.date(2020, 12, 28),
 datetime.date(2020, 12, 29),
 datetime.date(2020, 12, 30),
 datetime.date(2020, 12, 31)]

For Python 3; xrange() is only for Python 2. Based on the answer from Dave Webb and includes code to show the days including weekends

import datetime

start_date = datetime.date(2014, 1, 1)
end_date = datetime.date(2014, 1, 16)

delta_days = (end_date - start_date).days
delta_days # 13

day_generator = (start_date + datetime.timedelta(x + 1) for x in range((end_date - start_date).days))
delta_days = sum(1 for day in day_generator if day.weekday() < 5)
delta_days # 10

For those also wanting to exclude public holidays without manually specifying them, one can use the holidays package along with busday_count from numpy.

from datetime import date 

import numpy as np
import holidays

np.busday_count(
    begindates=date(2021, 1, 1),
    enddates=date(2021, 3, 20),
    holidays=list(
        holidays.US(state="CA", years=2021).keys()
    ),
)

if you want a date, x business days away from a known date, you can do

import datetime
from pandas.tseries.offsets import BusinessDay
known_date = datetime.date(2022, 6, 7)
x = 7 # 7 business days away

date = known_date - 7 * BusinessDay() # Timestamp('2022-05-27 00:00:00')

if you want the series of dates

import pandas as pd
date_series  = pd.bdate_range(date, known_date) # DatetimeIndex(['2022-05-27', '2022-05-30', '2022-05-31', '2022-06-01','2022-06-02', '2022-06-03','2022-06-06', '2022-06-07'],dtype='datetime64[ns]', freq='B')

My solution is also counting the last day. So if start and end are set to the same weekday then the asnwer will be 1 (eg 17th Oct both). If start and end are 2 consecutive weekdays then answer will be 2 (eg for 17th and 18th Oct). It counts the whole weeks (in each we will have 2 weekend days) and then check reminder days if they contain weekend days.

import datetime

def getWeekdaysNumber(start,end):
    numberOfDays = (end-start).days+1
    numberOfWeeks = numberOfDays // 7
    reminderDays = numberOfDays % 7
    numberOfDays -= numberOfWeeks *2
    if reminderDays:
        #this line is creating a set of weekdays for remainder days where 7 and 0 will be Saturday, 6 and -1 will be Sunday
        weekdays = set(range(end.isoweekday(), end.isoweekday() - reminderDays, -1))
        numberOfDays -= len(weekdays.intersection([7,6,0,-1])
    return numberOfDays 

usage example:

start = date(2018,10,10)
end = date (2018,10,17)
result = getWeekdaysNumber(start,end)`
Related