Reputation: 53
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
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
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:
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