4

I have a function to return the difference between 2 dates, however I need to work out the difference in working hours, assuming Monday to Friday (9am to 5:30pm):

//DATE DIFF FUNCTION
// Set timezone
date_default_timezone_set("GMT");

// Time format is UNIX timestamp or
// PHP strtotime compatible strings
function dateDiff($time1, $time2, $precision = 6) {
    // If not numeric then convert texts to unix timestamps
    if (!is_int($time1)) {
        $time1 = strtotime($time1);
    }
    if (!is_int($time2)) {
        $time2 = strtotime($time2);
    }

    // If time1 is bigger than time2
    // Then swap time1 and time2
    if ($time1 > $time2) {
        $ttime = $time1;
        $time1 = $time2;
        $time2 = $ttime;
    }

    // Set up intervals and diffs arrays
    $intervals = array('year','month','day','hour','minute','second');
    $diffs = array();

    // Loop thru all intervals
    foreach ($intervals as $interval) {
        // Set default diff to 0
        $diffs[$interval] = 0;
        // Create temp time from time1 and interval
        $ttime = strtotime("+1 " . $interval, $time1);
        // Loop until temp time is smaller than time2
        while ($time2 >= $ttime) {
            $time1 = $ttime;
            $diffs[$interval]++;
            // Create new temp time from time1 and interval
            $ttime = strtotime("+1 " . $interval, $time1);
        }
    }

    $count = 0;
    $times = array();
    // Loop thru all diffs
    foreach ($diffs as $interval => $value) {
        // Break if we have needed precission
        if ($count >= $precision) {
            break;
        }
        // Add value and interval 
        // if value is bigger than 0
        if ($value > 0) {
            // Add s if value is not 1
            if ($value != 1) {
                $interval .= "s";
            }
            // Add value and interval to times array
            $times[] = $value . " " . $interval;
            $count++;
        }
    }

    // Return string with times
    return implode(", ", $times);
}

Date 1 = 2012-03-24 03:58:58
Date 2 = 2012-03-22 11:29:16

Is there a simple way of doing this, i.e - calculating the percentage of working hours in a week and dividing the difference using the above function - I have played around with this idea and got some very strange figures...

Or is there better way....?

John Conde
  • 217,595
  • 99
  • 455
  • 496
user1250812
  • 75
  • 1
  • 7
  • You can't do a percentage of the week kind of thing: If you have a full Saturday and Sunday, that is 0% of a work week, about 29% of a week. The fastest way to do this is to figure out the full days worked and then calculate the partial days before and after those full days worked. – Jerome Jul 09 '12 at 14:46

3 Answers3

4

This example uses PHP's built in DateTime classes to do the date math. How I approached this was to start by counting the number of full working days between the two dates and then multiply that by 8 (see notes). Then it gets the hours worked on the partial days and adds them to the total hours worked. Turning this into a function would be fairly straightforward to do.

Notes:

  • Does not take timestamps into account. But you already know how to do that.
  • Does not handle holidays. (That can be easily added by using an array of holidays and adding it to where you filter out Saturdays and Sundays).
  • Requires PHP 5.3.6+
  • Assumes an 8 hour workday. If employees do not take lunch change $hours = $days * 8; to $hours = $days * 8.5;

.

<?php
// Initial datetimes
$date1 = new DateTime('2012-03-22 11:29:16');
$date2 = new DateTime('2012-03-24 03:58:58');

// Set first datetime to midnight of next day
$start = clone $date1;
$start->modify('+1 day');
$start->modify('midnight');

// Set second datetime to midnight of that day
$end = clone $date2;
$end->modify('midnight');

// Count the number of full days between both dates
$days = 0;

// Loop through each day between two dates
$interval = new DateInterval('P1D');
$period = new DatePeriod($start, $interval, $end);
foreach ($period as $dt) {
    // If it is a weekend don't count it
    if (!in_array($dt->format('l'), array('Saturday', 'Sunday'))) {
        $days++;
    }
}

// Assume 8 hour workdays
$hours = $days * 8;

// Get the number of hours worked on the first day
$date1->modify('5:30 PM');
$diff = $date1->diff($start);
$hours += $diff->h;

// Get the number of hours worked the second day
$date1->modify('8 AM');
$diff = $date2->diff($end);
$hours += $diff->h;

echo $hours;

See it in action

Reference

John Conde
  • 217,595
  • 99
  • 455
  • 496
  • What functionality requires 5.3.6? – Theodore R. Smith Dec 19 '13 at 22:15
  • PHPgolf's PHP version: 5.3.3-phpGolf http://www.phpgolf.org/doc That's from July 2010! 3 1/2 years out of date ;o – Theodore R. Smith Dec 19 '13 at 22:23
  • @TheodoreR.Smith The DateTime functionality. If you run this in an earlier version of PHP 5.3 you will get incorrect results. Click on the 'see it in action" link to see what I mean. – John Conde Dec 20 '13 at 00:42
  • I have tried to add fixed holidays to the bove solution. I have tried : if($date > $startofday && $date <= $endofday && !in_array($date->format('n'), array(5,6,7))){ $count++; } but it gives only the number of the days. tried to use 'j' instead but couldn't figure out the correct format. I have tried : if($date > $startofday && $date <= $endofday && !in_array($date->format('j'), array(5,6,7,9))){ $count++; } meaning 5th, 6th, 7th, 9th. any suggestions ? – Kissa Mia May 17 '17 at 23:54
2

Here's what I've come up with.

My solution checks the start and end times of the original dates, and adjusts them according to the actual start and end times of the work day (if the original start time is before work's opening time, it sets it to the latter).

After this is done to both start and end times, the times are compared to retrieve a DateInterval diff, calculating the total days, hours, etc. The date range is then checked for any weekend days, and if found, one total day is reduced from the diff.

Finally, the hours are calculated as commented. :)

Cheers to John for inspiring some of this solution, particularly the DatePeriod to check for weekends.

Gold star to anyone who breaks this; I'll be happy to update if anyone finds a loophole!


Gold star to myself, I broke it! Yeah, weekends are still buggy (try starting at 4pm on Saturday and ending at 1pm Monday). I will conquer you, work hours problem!

Ninja edit #2: I think I took care of the weekend bugs by reverting the start and end times to the most recent respective weekday if they fall on a weekend. Got good results after testing a handful of date ranges (starting and ending on the same weekend barfs, as expected). I'm not entirely convinced this is as optimized / simple as it could be, but at least it works better now.


// Settings
$workStartHour = 9;
$workStartMin = 0;
$workEndHour = 17;
$workEndMin = 30;
$workdayHours = 8.5;
$weekends = ['Saturday', 'Sunday'];
$hours = 0;

// Original start and end times, and their clones that we'll modify.
$originalStart = new DateTime('2012-03-22 11:29:16');
$start = clone $originalStart;

// Starting on a weekend? Skip to a weekday.
while (in_array($start->format('l'), $weekends))
{
    $start->modify('midnight tomorrow');
}

$originalEnd = new DateTime('2012-03-24 03:58:58');
$end = clone $originalEnd;

// Ending on a weekend? Go back to a weekday.
while (in_array($end->format('l'), $weekends))
{
    $end->modify('-1 day')->setTime(23, 59);
}

// Is the start date after the end date? Might happen if start and end
// are on the same weekend (whoops).
if ($start > $end) throw new Exception('Start date is AFTER end date!');

// Are the times outside of normal work hours? If so, adjust.
$startAdj = clone $start;

if ($start < $startAdj->setTime($workStartHour, $workStartMin))
{
    // Start is earlier; adjust to real start time.
    $start = $startAdj;
}
else if ($start > $startAdj->setTime($workEndHour, $workEndMin))
{
    // Start is after close of that day, move to tomorrow.
    $start = $startAdj->setTime($workStartHour, $workStartMin)->modify('+1 day');
}

$endAdj = clone $end;

if ($end > $endAdj->setTime($workEndHour, $workEndMin))
{
    // End is after; adjust to real end time.
    $end = $endAdj;
}
else if ($end < $endAdj->setTime($workStartHour, $workStartMin))
{
    // End is before start of that day, move to day before.
    $end = $endAdj->setTime($workEndHour, $workEndMin)->modify('-1 day');
}

// Calculate the difference between our modified days.
$diff = $start->diff($end);

// Go through each day using the original values, so we can check for weekends.
$period = new DatePeriod($start, new DateInterval('P1D'), $end);

foreach ($period as $day)
{
    // If it's a weekend day, take it out of our total days in the diff.
    if (in_array($day->format('l'), ['Saturday', 'Sunday'])) $diff->d--;
}

// Calculate! Days * Hours in a day + hours + minutes converted to hours.
$hours = ($diff->d * $workdayHours) + $diff->h + round($diff->i / 60, 2);
Community
  • 1
  • 1
Aken Roberts
  • 13,012
  • 3
  • 34
  • 40
  • Both these answers are wrong, can somebody please answer the question correctly. – user794846 Jun 30 '14 at 14:48
  • Not with attitude like that. – Aken Roberts Jun 30 '14 at 17:48
  • @Cryode This is the only code I have found that works 100% of the time(as far as I can tell). All the other answers both on this question and the numerous others have barfed at certain date ranges (for example `2017-01-24 00:00:00` - `2017-01-30 09:45:00` didn't work on any other answer!). THANK YOU! – superphonic Feb 17 '17 at 14:07
  • Scratch that, I just broke it. `2017-02-08 19:00:00` - `2017-02-08 22:00:00` results in 16 hours – superphonic Feb 17 '17 at 14:19
  • @superphonic I forgot about this answer. Gold star for you for finding another issue! Let me know if you find a solution for it. :) – Aken Roberts Feb 17 '17 at 17:56
1

As the old saying goes "if you want something done right do it yourself". Not saying this is optimal but its atleast returning the correct amount of hours for me.

function biss_hours($start, $end){

    $startDate = new DateTime($start);
    $endDate = new DateTime($end);
    $periodInterval = new DateInterval( "PT1H" );

    $period = new DatePeriod( $startDate, $periodInterval, $endDate );
    $count = 0;

      foreach($period as $date){

           $startofday = clone $date;
           $startofday->setTime(8,30);

           $endofday = clone $date;
           $endofday->setTime(17,30);

    if($date > $startofday && $date <= $endofday && !in_array($date->format('l'), array('Sunday','Saturday'))){

        $count++;
    }

}

//Get seconds of Start time
$start_d = date("Y-m-d H:00:00", strtotime($start));
$start_d_seconds = strtotime($start_d);
$start_t_seconds = strtotime($start);
$start_seconds = $start_t_seconds - $start_d_seconds;

//Get seconds of End time
$end_d = date("Y-m-d H:00:00", strtotime($end));
$end_d_seconds = strtotime($end_d);
$end_t_seconds = strtotime($end);
$end_seconds = $end_t_seconds - $end_d_seconds;

$diff = $end_seconds-$start_seconds;

if($diff!=0):
    $count--;
endif;

$total_min_sec = date('i:s',$diff);

return $count .":".$total_min_sec;
}

$start = '2014-06-23 12:30:00';
$end = '2014-06-27 15:45:00';

$go = biss_hours($start,$end);
echo $go;
user794846
  • 1,881
  • 5
  • 29
  • 72