2
\$\begingroup\$

Edit

I noticed some negative values being calculated lately. I'm still unsure why it's happening. Investigating and will post back when I have an answer.


I'm calculating the time it took a consultant to respond to a client using a start time and end time as input. I need to strip out any after hours, weekend, holidays etc and end up with only the time it took within business hours.

The basics of my function:

  1. Starting with the start date, I loop through it one day at a time until it reaches the same date as the end date.
  2. In my case 8.5 hours would be added for every day that passes in the loop.
  3. If there is a weekend/holiday in between the function just increments the date and does not add any additional seconds to the total time.
  4. If the loop reaches the end, it calculates the difference between the starting hour and end time and adds it to the total time.

Is this the most effective solution or can I improve?

I know there are a couple of answers and solutions out there, they did help me to get started but it wasn't inline with what I needed.

Below is my full code. The last function (businessHours) is the main function used for calculating the time.

This function just holds the basic settings like start & end time of a business day

function businessHours_settings()
{
    // settings
    $start = "08:00";
    $end   = "16:30";

    // calculate amount of hours per working day
    $diff  = strtotime( $end ) - strtotime( $start );
    $hours = $diff / 3600;

    $settings = array(
        "start" => $start,
        "end"   => $end,
        "diff"  => $diff,
        "hrs"   => $hours
    );

    return $settings;
}

This function returns a boolean whether the start time is within business hours

function _isWorkingHour( $time )
{
    $settings = businessHours_settings();

    if ( date("H", $time) >= date("H", strtotime( $settings['start'] )) && date("H", $time) <= date("H", strtotime( $settings['end'] )) )
    {
        return true;
    }
    else {
        return false;
    }
}

function _isPublicHoliday( $timestamp, $incName = false )
{   
    $ts       = strtotime($timestamp);
    $date     = date("d-m-Y", $ts );
    $mnth     = date("m", $ts );

    if ( $mnth == "12" )
    {
        $currYear = date("Y");
        $nextYear = date("Y", strtotime( $currYear . " +365 days") );
    }
    elseif ( $mnth == "01" || $mnth == "1" )
    {
        $currYear = date("Y", strtotime( $currYear . " -365 days") );
        $nextYear = date("Y");
    }

This function returns all public holidays

    // Public holidays of South Africa
    $holidays = array(
        "Human Rights Day"      => "21-03-$currYear",
        "Good Friday"           => "30-03-$currYear",
        "Family Day"            => "02-04-$currYear",
        "Freedom Day"           => "27-04-$currYear",
        "Labour Day"            => "01-05-$currYear",
        "Youth Day"             => "16-06-$currYear",
        "National Women's Day"  => "09-08-$currYear",
        "Heritage Day"          => "24-09-$currYear",
        "Day of Reconciliation" => "16-12-$currYear"
    );
    // Dates when company closes
    $decemberHolidays = array(
        "start" => strtotime("15-12-$currYear"),
        "end"   => strtotime("08-01-$nextYear")
    );

    if ( $ts > $decemberHolidays['start'] && $ts < $decemberHolidays['end'] )
    {
        return true;
    }

    $exists = array_search($date, $holidays);

    if ( $exists !== false )
    {
        if ($incName)
        {
            return $exists;
        }
        else 
        {
            return true;
        }
    }
    else {
        return false;
    }
}

This is the main function that calculates the amount of secs between the start & end time.

function businessHours( $start, $end )
{
    // settings
    $settings = businessHours_settings();

    $dayStart  = $settings['start']; 
    $dayEnd    = $settings['end'];
    $diff      = $settings['diff'];
    $hourInDay = $settings['hrs'];

    // timestamps
    $start = strtotime($start);
    $end   = strtotime($end);

    // if timestamps = false return original
    if ( !$start || !$end ) return $end - $start;

    $addDay    = "";
    $counter = $totalHours = $addTime = 0;
    $secInDay  = 60 * 60 * 24;  

    $dayDiff   = ( $end - $start ) / $secInDay;
    $dayDiffF  = floor($dayDiff);

    // check if start date is within working hours
    $checkStart = _isWorkingHour($start);

    if ( $checkStart && date("Y m d", $end) == date("Y m d", $start)  )
    {
        return $end - $start;
    }
    else 
    {
        if ( $checkStart === false ) 
        {
            // Move start time until 08:00
            if ( date("G", $start) < 8 )
            {
                $start = strtotime( date("Y-m-d $dayStart", $start) );
            }
            elseif ( date("G", $start) <= 16 )
            {
                $start = strtotime( date("Y-m-d $dayStart", $start) . " +1 day" );
            }
        }
        elseif ( $checkStart === true )
        {
            $addTime =  $start - strtotime( date("Y-m-d $dayStart", $start) );
            $start   = strtotime( date("Y-m-d $dayStart", $start) );
        }

        $limit = date("j", $end);
        $itrtr = date("j", $start);

        while ( $itrtr < $limit )
        {
            if ( date("Y m d", $end) == date("Y m d", $start) ) break;

            if ( 
                date("N", strtotime( date("Y-m-d H:i:s", $start) . $addDay )) == "5" || 
                date("N", strtotime( date("Y-m-d H:i:s", $start) . $addDay )) == "6" || 
                _isPublicHoliday( date("N", strtotime( date("Y-m-d H:i:s", $start) . $addDay ))) ||
                ( date("N", strtotime( date("Y-m-d H:i:s", $start) . $addDay ))  )
            ){
                $itrtr++;
                continue;
            } 
            $totalHours += $diff;
            $addDay = " +1 day";
            $itrtr++;
        }
        $fEnd    = strtotime( date("Y-m-d H:i", $end) );
        $sEnd    = strtotime( date("Y-m-d $dayStart", $end) );
        $endTime = $fEnd - $sEnd;

        $totalHours += $endTime;
        $totalHours -= $addTime;
    }
    return $totalHours;
}
\$\endgroup\$
  • \$\begingroup\$ Are you using composer and autoloading? Also, what version of PHP are you using? \$\endgroup\$ – Rwd May 30 '18 at 7:43
  • \$\begingroup\$ Would you be able to give a couple of example $start and $end times that you would pass to your businessHours function as well? \$\endgroup\$ – Rwd May 30 '18 at 8:04
  • \$\begingroup\$ @RossWilson im not using composer. Using PHP 7.0.30. The start and end dates I use is in the format 2018-05-02 11:12:33 \$\endgroup\$ – Chris May 30 '18 at 9:16
  • \$\begingroup\$ Cheers. I'm assuming that the time between start and end dates could be a few days in theory? \$\endgroup\$ – Rwd May 30 '18 at 9:18
  • \$\begingroup\$ Yes that correct, sometimes even a week, with weekends, holidays etc in between \$\endgroup\$ – Chris May 30 '18 at 9:20
2
\$\begingroup\$

You could make use of PHP's DateTime class and have something like:

/**
 * Check if the given DateTime object is a business day.
 *
 * @param DateTime $date
 * @return bool
 */
function isBusinessDay(DateTime $date)
{
    //Weekends
    if ($date->format('N') > 5) {
        return false;
    }

    //Hard coded public Holidays
    $holidays = [
        "Human Rights Day"      => new DateTime(date('Y') . '-03-21'),
        "Good Friday"           => new DateTime(date('Y') . '-03-30'),
        "Family Day"            => new DateTime(date('Y') . '-04-02'),
        "Freedom Day"           => new DateTime(date('Y') . '-04-27'),
        "Labour Day"            => new DateTime(date('Y') . '-05-01'),
        "Youth Day"             => new DateTime(date('Y') . '-06-16'),
        "National Women's Day"  => new DateTime(date('Y') . '-08-09'),
        "Heritage Day"          => new DateTime(date('Y') . '-09-24'),
        "Day of Reconciliation" => new DateTime(date('Y') . '-12-16'),
    ];

    foreach ($holidays as $holiday) {
        if ($holiday->format('Y-m-d') === $date->format('Y-m-d')) {
            return false;
        }
    }

    //December company holidays
    if (new DateTime(date('Y') . '-12-15') <= $date && $date <= new DateTime((date('Y') + 1) . '-01-08')) {
        return false;
    }

    // Other checks can go here

    return true;
}

/**
 * Get the available business time between two dates (in seconds).
 *
 * @param $start
 * @param $end
 * @return mixed
 */
function businessTime($start, $end)
{
    $start = $start instanceof \DateTime ? $start : new DateTime($start);
    $end = $end instanceof \DateTime ? $end : new DateTime($end);
    $dates = [];

    $date = clone $start;

    while ($date <= $end) {

        $datesEnd = (clone $date)->setTime(23, 59, 59);

        if (isBusinessDay($date)) {
            $dates[] = (object)[
                'start' => clone $date,
                'end'   => clone ($end < $datesEnd ? $end : $datesEnd),
            ];
        }

        $date->modify('+1 day')->setTime(0, 0, 0);
    }

    return array_reduce($dates, function ($carry, $item) {

        $businessStart = (clone $item->start)->setTime(8, 0, 0);
        $businessEnd = (clone $item->start)->setTime(16, 30, 0);

        $start = $item->start < $businessStart ? $businessStart : $item->start;
        $end = $item->end > $businessEnd ? $businessEnd : $item->end;

        //Diff in seconds
        return $carry += max(0, $end->getTimestamp() - $start->getTimestamp());
    }, 0);
}

$seconds = businessTime('2018-01-01 10:00:00', '2018-01-03 15:00:00');

echo $seconds;
\$\endgroup\$
  • \$\begingroup\$ Thanx Ross, your example looks awesome, much more clean and elegant. I have implemented it and will revert back once the numbers are coming in! \$\endgroup\$ – Chris Jun 8 '18 at 11:23
  • \$\begingroup\$ Thanx again Ross, I checked the values that came in. Yours is much more accurate than my original function but it still returns negative values. Im trying to scan through the function to check why, but not sure. Do you know why it would return negative values? Run this as an example: $start = '2018-05-02 20:52:19'; $end = '2018-05-03 10:54:22'; $b = businessTime($start, $end); echo $b; \$\endgroup\$ – Chris Jun 8 '18 at 13:10
  • \$\begingroup\$ @Chris I've updated my answer. I'd just forgotten to account for start times that happen after the end of the business day. \$\endgroup\$ – Rwd Jun 8 '18 at 14:20
  • \$\begingroup\$ Thanx for the update Ross, I tested your function and its SOLID! \$\endgroup\$ – Chris Jun 11 '18 at 7:35

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.