calc the working hours between 2 dates in PHP

2020-01-29 16:39发布

问题:

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....?

回答1:

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

  • DateTime Class
  • DatePeriod Class
  • DateInterval Class


回答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);


回答3:

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;