Calculate Business Hours

Don’t you hate it when customers ask for one thing; then a week after they get it they want something else – and you knew it all along?

Yeah me too. Just like when working on a data management application the customers said they wanted reporting. They agreed that the reporting would be based on normal calendar days. That was fine.

About a week after implementation they decided that since they didn’t operate 24×7, their reports should be by business hours. [Oh, that's just great...]

The application was written in Perl. So I figured there was something is CPAN just for the occasion. Sure enough there was.

First I tried the Date::Calc module. I liked the idea that it would work with Date::Manip to automatically handle company holidays.

It worked, but ran waaay too slow. It was fine for a single calculation, but this had to be performed for hundreds or thousands of records. It took several minutes to complete on the old server we had to use.

So instead of wasting more time testing other modules, I just jumped in and wrote one myself.

To my surprise, my homemade version worked extremely fast – at least compared to the Date::Calc version.

To be fair I did not test the Business::Hours module. After looking at the source code, it takes a similar approach to my version – but does not currently exclude holidays.

Here is my solution. One of these days I might make it Object Oriented and turn it into an official module. If I ever get time…

Note: It assumes workday is 08:00 – 17:00 (8am – 5pm). It does not currently have a way to set this dynamically. [If I ever get time... blah blah blah - you know]

Perl source:

sub calcBusinessDays
{
# calculates business day time difference between two epoch times
# takes to input parameters, example: calcBusinessDays($epoch1, $epoch2)

use Date::Manip;
use Time::Local;
die print('Program Error: Invalid call to calcBusinessDays().') unless (scalar(@_) == 2);

my ($endepoch, $startepoch, $businesstime, $thistime, @START, @END);

# determine start and end
if ( $_[1] > $_[0] )
{
$endepoch = $_[1];
$startepoch = $_[0];
}
else
{
$endepoch = $_[0];
$startepoch = $_[1];
}

#load holidays from Date::Manip
Date_Init('GlobalCnf=/path/to/holiday-config-file',
'WorkWeekBeg=1',
'WorkWeekEnd=5',
'WorkDayBeg=08:00',
'WorkDayEnd=17:00');

$businesstime = 0; #initialize time duration to zero

# get date/time details
@END = localtime($endepoch);
@START = localtime($startepoch);

# if same day / shortcut
if ( $END[7] == $START[7] )
{
# same day calculation
my @checkdate = @START;

$checkdate[4]++; # increase month val to normal val
$checkdate[4] = sprintf('%02d', $checkdate[4]);
$checkdate[3] = sprintf('%02d', $checkdate[3]);
$checkdate[5] += 1900; # increase year to normal val

# only do if it is a valid workday according to Date::Manip
if (Date_IsWorkDay(ParseDate($checkdate[5].$checkdate[4].$checkdate[3])))
{
# if start time before 8AM set it to 8
if ( $START[2] < 8 )
{
# set start to 8
$startepoch = timelocal( 0,0,8,$START[3],$START[4],$START[5] );
}
elsif ( $START[2] >= 17 )
{
# if time is 5PM or after set it to 4:59PM, Used 4:59PM because using
# 5:00 will get 1 extra second since we include 8:00.
$startepoch = timelocal(0,59,16,$START[3],$START[4],$START[5]);
}

if ( $END[2] < 8 )
{
# set start to 8
$endepoch = timelocal(0,0,8,$END[3],$END[4],$END[5]);
}
elsif ( $END[2] >= 17 )
{
# if time is 5PM or after set it to 4:59PM, Used 4:59PM because using
# 5:00 will get 1 extra second since we include 8:00;
$endepoch = timelocal(0,59,16,$END[3],$END[4],$END[5]);
}
$businesstime = $endepoch - $startepoch;
}

}
else
{
# different day calculation

# Begin with epoch of start day. Used 1:00am instead of 00:00am because
# some days have more 86400 seconds due to catch-up times.

my $thisepoch = timelocal(0,0,1,$START[3],$START[4],$START[5]);

my $continue = 1;
while ( $continue ==1 )
{
my @THISTIME = localtime($thisepoch);
my @checkdate = @THISTIME;
my $isworkday = 0;
my $thisbusinesstime = 0;

$checkdate[4]++; # increase month val to normal val
$checkdate[4] = sprintf('%02d', $checkdate[4]);
$checkdate[3] = sprintf('%02d', $checkdate[3]);
$checkdate[5] += 1900; # increase year to normal val

# check for holiday using date in 'YYYYMMDD' format;
if (Date_IsWorkDay(ParseDate($checkdate[5].$checkdate[4].$checkdate[3])))
{
$isworkday = 1;
}

if ( $THISTIME[7] == $START[7] )
{
# first day

# if stated before 8 on first day add a full day
if ( $START[2] < = 8 )
{
$thisbusinesstime += 8 * 3600;
}
elsif ( $START[2] < 17 )
{
# if start time is before 5, then add difference in start time and 5
my $eepoch = timelocal(0,59,16,$START[3],$START[4],$START[5]);
$thisbusinesstime += $eepoch - $startepoch;
}

}
elsif ( $THISTIME[7] == $END[7] )
{
# last day

# if ended after 5 on last day, add full day
if ( $END[2] >= 17 )
{
$thisbusinesstime += 8 * 3600;
}
elsif ( $END[2] >= 8 )
{
# if end time is 8 or later, add difference in endepoch and 8am
my $sepoch = timelocal(0,0,8,$END[3],$END[4],$END[5]);
$thisbusinesstime += $endepoch - $sepoch;
}
$continue = 0; # stop on last day
}
else
{
# if not first day or last day add a full business day;
$thisbusinesstime += 8 * 3600;
}

# if valid workday, add to total
if ( $isworkday == 1 )
{
$businesstime += $thisbusinesstime;
}
$thisepoch += 86400; # go to next day;
}

}
return $businesstime;
}

One Response to “Calculate Business Hours”

  1. Dad Says:

    Looks like alot of work. My little girl sure is cute. Love Dad.

Leave a Reply

You must be logged in to post a comment.