Dec 112019
 

Struggling trying to come up with a solution to calculate the time difference between two datetimes, with respect to when a business is open? I was too.

First approach I looked into was a package that does just this: https://packagist.org/packages/hughgrigg/php-business-time . Unfortunately it doesn’t allow much configuration for constraints. Ie, if you have different hours on a weekday than you do a weekend then you’re out of luck.

Second approach was to use the diffFiltered function that’s part of Carbon. My solution looked like this:

$minutes = $fromTime->diffFiltered(CarbonInterval::minute(), function(Carbon $date) {
            return $this->isOpen($date);
        }, $toTime, false);

There are two problems with this approach. First, it’s slow. It works by iterating through your time interval one by one. That means if you have a huge span between your fromTime and toTime, you’re going to be wasting a lot of cycles.

Second, there’s a constant in the Carbon class: NEXT_MAX_ATTEMPTS = 1000; This means we can’t go more than 1000 iterations without returning a valid date otherwise we’ll get “RuntimeException: Could not find next valid date”. This effectively prevents us to measuring anything more than a day, which doesn’t work for us considering businesses can be closed on weekends, or over holidays.

We can overcome both of those concerns by changing the CarbonInterval to a bigger value, such as CarbonInterval::hour(), or even CarbonInterval::minute(5). The downside of this is we lose precision. Everything is now chunked into that interval length, so (in the case of minute(5)) we wouldn’t get anything more accurate than 0, 5, 10, 15, etc.

Best Approach

What first seemed like a hard problem didn’t turn out to be too bad if I took the time to think through it. If you simply iterate through your time span a day at a time, and consider the open and close time of your business, the logic isn’t that tricky. Here’s the code I used

/**
 * Returns the number of minutes between two datetimes, excluding time the facility was closed
 * This method works by iterating through each day and comparing open/close times of facility
 *
 * @param Carbon|null $fromTime
 * @param Carbon|null $toTime
 * @return int $minutes
 */
public function minutesDiffWithinOpenHours(Carbon $fromTime, Carbon $toTime)
{
    //prevent time travel
    if($fromTime >= $toTime) {
        return 0;
    }

    $minutes = 0;
    $loopTime = $fromTime;

    while($loopTime < $toTime) {
        $openTime = $this->openTime($loopTime);
        $closeTime = $this->closeTime($loopTime);

        if ($openTime && $closeTime) {
            $calcFromTime = max($loopTime, $openTime);
            $calcToTime = min($toTime, $closeTime);

            if ($calcFromTime < $calcToTime) {
                $minutes += $calcFromTime->diffInMinutes($calcToTime);
            }
            //else from is after hours, don't count
        }
        //else facility is closed all day, don't count

        $loopTime = Carbon::parse($loopTime->addDay()->toDateString().' 00:00:01', $this->timezone);
    }

    return $minutes;
}

Note that it’s dependent on you coming up with your own implementations of openTime() and closeTime(), which returns a Carbon datetime of the open/close time on that date.

What’s great about this is it’s fast, and it’s precise. Win-win!

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)