Here is what I came up with.
My solution checks the start and end time of the original dates and sets them in accordance with the actual start and end time of the working day (if the original start time is before the opening of the working time, it sets it to the last).
After that, for both the beginning and the end, the time is compared to extracting DateInterval diff, calculating the total days, hours, etc. Then, the date range is checked for any weekend, and if it is found, one common day is reduced from the difference.
Finally, the hours are calculated as commented out. :)
Welcomes John for inspiring some of these solutions, especially DatePeriod for the weekend.
A golden star for all who violate this; I will be happy to update if anyone finds a loophole!
Gold star for myself, I broke it! Yes, the weekend still doesnβt work (try starting at 16:00 on Saturday and ending at 13:00 on Monday). I will defeat you, the problem of working hours!
Ninja edit # 2: I think I took care of the weekend errors by returning the start and end time to the very last relevant weekday if they fall on the weekend. Got good results after testing several date ranges (starting and ending on the same output bars, as expected). I am not entirely convinced that it is as optimized / simple as it could be, but at least now it works better.
// 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 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);