merthay
merthay

Reputation: 53

Time between two dates except weekends

I need a function to calculate time in seconds between two dates except weekend days, something that will work like this:

# friday 9 PM
start_date = datetime.datetime(2015, 9, 18, 21, 0, 0)

# monday 3 AM
end_date = datetime.datetime(2015, 9, 21, 3, 0, 0)

# should return 6 hours
time = time_between_two_dates_except_weekends(start_date, end_date)

I implemented my own function, which is works, but it seems unnecessarily huge and complicated. I think it can be more simpler.

import datetime

from dateutil.relativedelta import relativedelta
from dateutil.rrule import DAILY, rrule

def time_between_two_dates_except_weekends(start_date, end_date):

    WEEKEND_DAYS = [5, 6]

    result = datetime.timedelta()

    if all([start_date.year == end_date.year, start_date.month == end_date.month, start_date.day == end_date.day]):
         result += datetime.timedelta(seconds = (end_date-start_date).seconds )
         return result

    day_after_start_date = start_date + relativedelta(days=1)
    day_after_start_date = day_after_start_date.replace(hour=0, minute=0, second=0)

    day_before_end_date = end_date - relativedelta(days=1)

    if start_date.weekday() not in WEEKEND_DAYS:
        result += datetime.timedelta(seconds = (day_after_start_date - start_date).total_seconds())

    dates_range = rrule(
        DAILY,
        byhour=0,
        byminute=0,
        bysecond=0,
        dtstart=day_after_start_date,
        until=day_before_end_date
    )

    for date in dates_range:
        if date.weekday() not in WEEKEND_DAYS:
            result += datetime.timedelta(seconds=24 * 60 * 60)

    if end_date.weekday() not in WEEKEND_DAYS:
        end_date_beginning = end_date.replace(hour=0, minute=0, second=0)
        result += datetime.timedelta(seconds = (end_date - end_date_beginning).total_seconds())

    return result

Is there any way to improve this?

UPD. turned out, not only my code is complicated but return incorrect result in some corner cases (for example when weekend day is passed for either the start or the end date). I recommend to just use code from correct answer below

Upvotes: 4

Views: 2688

Answers (2)

Padraic Cunningham
Padraic Cunningham

Reputation: 180550

from datetime import timedelta
def diff(s, e):
    _diff = (end_date - start_date)
    while s < e:
        if s.weekday() in {5, 6}:
            _diff -= timedelta(days=1)
        s += timedelta(days=1)
    return timedelta(seconds=_diff.total_seconds())

If your dates can end or start on a weekend we need to move them to the next monday which we can use a helper function to do:

from datetime import timedelta

def helper(d):
   if d.weekday() == 5:
        d += timedelta(days=1)
    return d.replace(hour=0, minute=0, second=0, microsecond=0)

def diff(s, e):
    if e.weekday() in {5, 6}:
        e = helper(e)
    if s.weekday() in {5, 6}:
        s = helper(s)
    _diff = (e - s)
    while s < e:
        if s.weekday() in {5, 6}:
            _diff -= timedelta(days=1)
        elif s.weekday() == 0:
            s += timedelta(days=4)
        s += timedelta(days=1)
    return timedelta(seconds=_diff.total_seconds())

Still runs a nice bit faster:

In [57]: timeit time_between_two_dates_except_weekends(start_date,end_date)
10 loops, best of 3: 95.5 ms per loop

In [58]: timeit diff(start_date,end_date)
100 loops, best of 3: 12.4 ms per loop

In [59]: diff(start_date,end_date)
Out[59]: datetime.timedelta(7699, 9300)

In [60]:  time_between_two_dates_except_weekends(start_date,end_date)
Out[60]: datetime.timedelta(7699, 9300)

Just doing the math:

from datetime import timedelta, datetime

def helper(d):
    if d.weekday() == 5:
        d += timedelta(days=1)
    return d.replace(hour=0, minute=0, second=0, microsecond=0)


def diff(s, e):
    weekend = {5, 6}
    both = e.weekday() in weekend and s.weekday() in weekend
    is_weekend = e.weekday() in {5, 6} or s.weekday() in {5, 6}
    if e.weekday() in weekend:
        e = helper(e)
    if s.weekday() in weekend:
        s = helper(s)
    _diff = (e - s)
    wek = _diff.days / 7 * 2 + is_weekend - both
    if s.weekday() > e.weekday() and not is_weekend:
        wek += 2
    return timedelta(seconds=_diff.total_seconds()) - timedelta(wek)

Which runs quite a bit faster:

In [2]: start_date = datetime(2016, 02, 29, 21, 25, 0)

In [3]: end_date = datetime(2045, 9, 02, 03, 56, 0)

In [4]: timeit diff(start_date,end_date)
100000 loops, best of 3: 6.8 µs per loop

In [5]: diff(start_date,end_date)
Out[5]: datetime.timedelta(7699, 9300)

Upvotes: 7

Travis
Travis

Reputation: 2058

I think this should do the trick:

import datetime
from dateutil.relativedelta import relativedelta, MO, SA

def time_between_two_dates_except_weekends(start_date, end_date):
    weekend = set([5, 6])

    if start_date.weekday() in weekend:
        start_date += relativedelta(weekday=MO(1))
        start_date = start_date.replace(hour=0,minute=0,second=0,microsecond=0)

    if end_date.weekday() in weekend:
        # One microsecond before Saturday at midnight.
        end_date = end_date + relativedelta(weekday=SA(-1)) - datetime.timedelta(microsecond=1)
        end_date = end_date.replace(hour=0,minute=0,second=0,microsecond=0) 

    number_of_weekends = (end_date - start_date).days / 7    
    if start_date.weekday() > end_date.weekday():
        number_of_weekends += 1

    return end_date - start_date -  datetime.timedelta(days=(number_of_weekends * 2)) 

start_date = datetime.datetime(2015, 8, 22, 14, 24, 29, 894810)
end_date = datetime.datetime.today()

print time_between_two_dates_except_weekends(start_date, end_date)

First, if the start or end dates are on the weekends, round them to the previous Friday or next Monday, then consider these two cases:

  • Full weeks intervening
  • Partial weeks intervening (there will only be one of these)

The full weeks case is easy, just take out two days for each week.

In the case of a partial week, notice that if the weekday() of the end date is greater than the weekday() of the start date, the number must "wrap around" and there must be a weekend intervening.

Upvotes: 1

Related Questions