3
\$\begingroup\$

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))
\$\endgroup\$
  • \$\begingroup\$ @Graipher - see my comment on your post. \$\endgroup\$ – Charon Jul 18 '16 at 17:33
2
\$\begingroup\$

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
\$\endgroup\$
  • \$\begingroup\$ 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). \$\endgroup\$ – Charon Jul 18 '16 at 17:33
  • \$\begingroup\$ I'm sure it will. That's a much more succint way of writing startday and endday though, so I'll steal it! \$\endgroup\$ – Charon Jul 18 '16 at 20:17
  • \$\begingroup\$ 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. \$\endgroup\$ – Charon Jul 18 '16 at 20:52
  • 1
    \$\begingroup\$ Whatever floats your boat. You can always add intermediate variables back in. \$\endgroup\$ – Graipher Jul 18 '16 at 20:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.