# Calculate working minutes between two timestamps

I've created a function to calculate the working minutes between two timestamps.

I class working minutes as those between 9am - 5pm and not on weekends or national holidays.

The holidays are those in the UK.

I think the function itself is quite quick, although generating the initial series probably takes a bit of time.

Because the working dates are fixed, I might just put them into a database table and load them each time the function is called, probably a bit faster.

import pandas as pd
import datetime
from pandas.tseries.holiday import Holiday, AbstractHolidayCalendar
from pandas.tseries.offsets import CDay

class HolidayCalendar(AbstractHolidayCalendar):
rules =[Holiday('Xmas Day generic',month=12,day=25),
Holiday('NYD generic',month=1,day=1),
Holiday('Boxing Day generic',month=12,day=26),
Holiday('Good Friday 2015',year=2015,month=4,day=3),
Holiday('Easter Monday 2015',year=2015,month=4,day=6),
Holiday('May Bank Holiday 2015',year=2015,month=5,day=4),
Holiday('Spring Bank Holiday 2015',year=2015,month=5,day=25),
Holiday('Summer Bank Holiday 2015',year=2015,month=8,day=31),
Holiday('Boxing Day 2015',year=2015,month=12,day=28),
Holiday('Good Friday 2016',year=2016,month=3,day=25),
Holiday('Easter Monday 2016',year=2016,month=3,day=28),
Holiday('May Bank 2016',year=2016,month=5,day=2),
Holiday('Spring Bank 2016',year=2016,month=5,day=30),
Holiday('Summer Bank 2016',year=2016,month=8,day=29),
Holiday('Boxing Day 2016',year=2016,month=12,day=26),
Holiday('Xmas Day 2016',year=2016,month=12,day=27),
Holiday('NYD 2017',year=2017,month=1,day=2),
Holiday('Good Friday 2017',year=2017,month=4,day=14),
Holiday('Easter Monday 2017',year=2017,month=4,day=17),
Holiday('May Bank 2017',year=2017,month=5,day=1),
Holiday('Spring Bank 2017',year=2017,month=5,day=29),
Holiday('Summer Bank 2017',year=2017,month=8,day=28)]

cal = HolidayCalendar()

dayindex = pd.bdate_range(datetime.date(2015,1,1),datetime.date.today(),freq=CDay(calendar=cal))

day_series = dayindex.to_series()

def count_mins(start,end):

starttime = datetime.datetime.fromtimestamp(int(start)/1000)

endtime = datetime.datetime.fromtimestamp(int(end)/1000)

days = day_series[starttime.date():endtime.date()]

daycount = len(days)

if daycount == 0:
return daycount
else:
startday = datetime.datetime(days[0].year,
days[0].month,
days[0].day,
hour=9,
minute=0)
endday = datetime.datetime(days[-1].year,
days[-1].month,
days[-1].day,
hour=17,
minute=0)
if daycount == 1:

if starttime < startday:
periodstart = startday
else:
periodstart = starttime
if endtime > endday:
periodend = endday
else:
periodend = endtime

return (periodend - periodstart).seconds/60

if daycount == 2:

if starttime < startday:
first_day_mins = 480
else:
first_day_mins = (startday.replace(hour=17)-starttime).seconds/60
if endtime > endday:
second_day_mins = 480
else:
second_day_mins = (endtime-endday.replace(hour=9)).seconds/60

return (first_day_mins + second_day_mins)

else:

if starttime < startday:
first_day_mins = 480
else:
first_day_mins = (startday.replace(hour=17)-starttime).seconds/60
if endtime > endday:
second_day_mins = 480
else:
second_day_mins = (endtime-endday.replace(hour=9)).seconds/60

return (first_day_mins + second_day_mins + ((daycount-2)*480))


Building on the advice from Graipher, I've settled on the below:

def count_mins(start,end):

starttime = datetime.datetime.fromtimestamp(int(start)/1000)

endtime = datetime.datetime.fromtimestamp(int(end)/1000)

days = day_series[starttime.date():endtime.date()]

daycount = len(days)

if not days:
return 0
else:
startday = days[0].replace(hour=9, minute=0)

endday = days[-1].replace(hour=17, minute=0)

if daycount == 1:

periodstart = max(startday, starttime)

periodend = min(endday, endtime)

return (periodend - periodstart).seconds/60

if daycount == 2:

first_day_mins = min(480, ((startday.replace(hour=17)-starttime).seconds/60))

second_day_mins = min(480, ((endtime-endday.replace(hour=9)).seconds/60))

return (first_day_mins + second_day_mins)

else:

first_day_mins = min(480, ((startday.replace(hour=17)-starttime).seconds/60))

last_day_mins = min(480, ((endtime-endday.replace(hour=9)).seconds/60))

return (first_day_mins + last_day_mins + ((daycount-2)*480))

• @Graipher - see my comment on your post. Commented Jul 18, 2016 at 17:33

A few points:

## Avoid magic numbers

The number 480 (number of minutes in a working day) appears repeatedly. Promote it to a CONSTANT.

## Superflous else

The following code can be shortened:

if cond:
return
else:
pass


to:

if cond:
return
pass


## Tuple assignment

You can use tuple assignment to get rid of some redundancy:

starttime, endtime = (datetime.datetime.fromtimestamp(int(t)/1000) for t in (start, end))


## Reduce logic

Your comparing logic can be greatly reduce.

First, note that:

if starttime < startday:
periodstart = startday
else:
periodstart = starttime


is equivalent to:

periodstart = max(starttime, startday)


Also the case for 1, 2 and more than 2 days are very similar. For the secon (daycount-2)*480 is just zero, for the first it is -1.

Also if daycount == 0 might be better written as if not days, since an empty list evaluates as false. This allows us to get rid of the variable daycount, since it is only needed once now.

With these few changes I arrive at (omitting the setup code for the holidays for brevity):

FULL_WORKDAY = 480  # min

def count_mins(start, end):
starttime, endtime = (datetime.datetime.fromtimestamp(int(t)/1000) for t in (start, end))
days = day_series[starttime.date():endtime.date()]
if not days:
return 0
first_day_mins = min(FULL_WORKDAY, (days[0].replace(hour9, minute=0, second=0) - starttime).seconds/60)
last_day_mins = min(FULL_WORKDAY, (endtime - days[-1].replace(hour=17, minute=0, second=0)).seconds/60)
return first_day_mins + (len(days)-2)*FULL_WORKDAY + last_day_mins

• Hey, thanks, I can see that your improvements would shorten my code dramatically. However, your logic contains an error. startday and endday should not be the same date as starttime and endtime; what if the latter are on a weekend? For example, with a start argument of 1468671650000 and an end argument of 1468833650000, you will get a wrong answer (should be 80). Commented Jul 18, 2016 at 17:33
• I'm sure it will. That's a much more succint way of writing startday and endday though, so I'll steal it! Commented Jul 18, 2016 at 20:17
• That's true, although I don't think that writing the function in that way will make it any faster (will it?) and certainly doesn't help the readability. Therefore, my final function would be as above. Commented Jul 18, 2016 at 20:52
• Whatever floats your boat. You can always add intermediate variables back in. Commented Jul 18, 2016 at 20:55