skip to Main Content

This script checks if a 60 min slot is available within a time range taking into account already booked slots.
It works fine for the first already booked slot but does not take into account the second. Also if I change the first booked slot to 12:10 it will not detect it.

Basically, the idea is if there is a 60 min slot between the range and the already booked slots to show it.

For example,, if we have a range 9:00 to 15:00 and we have two booked slots from 11:10 and 14:00, to return available slots from 9:00, 10:00 and 12:10

$start_time = '2022-10-21 09:00:00';  //start time as string
$end_time = '2022-10-21 15:00:00';  //end time as string
$booked = ['2022-10-21 12:00:00','2022-10-21 13:00:00']; //booked slots as arrays
$start = DateTime::createFromFormat('Y-m-d H:i:s',$start_time); //create date time objects
$end = DateTime::createFromFormat('Y-m-d H:i:s',$end_time);  //create date time objects
$time1 = $start;
$count = 0;  //number of slots
$out = array();   //array of slots 
for($i = $start; $i<$end;)  //for loop 
{
    $avoid = false; 
    $t1 = date_timestamp_get($i);
    $t2 = $t1+(60*60);

    for($k=0;$k<sizeof($booked);$k+=2)  //if booked hour
    {
        $st = DateTime::createFromFormat('Y-m-d H:i:s',$booked[$k]);
        $en = DateTime::createFromFormat('Y-m-d H:i:s',$booked[$k+1]);
          
        if( $t1 >= date_timestamp_get($st) && $t2 <= date_timestamp_get($en)  )
        $avoid = true;   //yes. booked
    }
    $slots =[ $i->format('H:i'),$i->modify("+60 minutes")->format('H:i')];
    if(!$avoid && $i<$end)  //if not booked and less than end time
    {
        $count++;  
        array_push($out,$slots);  //add slot to array
    }
}
var_dump($out);   //array out

Here is link to sandbox
https://onlinephp.io/c/b0b77

Additional information

Let’s say that we have a free time frame from 10:00 until 13:00. What the script should return is 10:00, 11:00, and 12:00 as possibilities if the time slot is 60 min.

If the timeslot is 45 min. for example, it should return 10:00, 10:45, 11:30, and 12:15.

So, the possible variants should start from the first possible option increasing with the time slot (45, 60, or whatever time slot is set)

The slot cannot start from 10:12, 10:44 or another custom time

4

Answers


  1. The algorithm you need to implement is as follows (I’m implementing it in pseudo-code for now):

    booking_length <- 1h //1 hour, you can set this to something else
    for dt <- start, dt <- end - booking_length, d <- d + booking_length
        b <- closest booking after d
        if (d + booking_length < b) then
            bookings.add(d)
        else
            d <- b + booking_length
        else
    end for
    

    The reason as of why I didn’t implement this in PHP was that I did not have enough time to do so at the time of this writing. Let me know if this is not clear yet and whether you need PHP code.

    Login or Signup to reply.
  2. It is simple nested loops:

    // input
    $start_time = '2022-10-21 09:00:00';
    $end_time = '2022-10-21 15:00:00';
    $slot_size = 60;
    $booked = array(
        array('start' => '2022-10-21 11:10:00', 'end' => '2022-10-21 12:10:00'),
        array('start' => '2022-10-21 14:00:00', 'end' => '2022-10-21 15:00:00')
    );
    // data must be sorted by date
    usort($booked, function($a, $b) {
        return DateTime::createFromFormat('Y-m-d H:i:s', $a['start']) <=> DateTime::createFromFormat('Y-m-d H:i:s', $b['start']);
    });
    // start with first slot and first booked
    $slotsize = new DateInterval('PT' . $slot_size . 'M');
    $t1 = DateTime::createFromFormat('Y-m-d H:i:s', $start_time);
    $t2 = DateTime::createFromFormat('Y-m-d H:i:s', $start_time)->add($slotsize);
    $i = 0;
    while (true) {
        if ($i < count($booked)) {
            // if booked slot exists then current loop ends at its start time
            $stoptime = DateTime::createFromFormat('Y-m-d H:i:s', $booked[$i]['start']);
        } else {
            // otherwise loop ends at the end time
            $stoptime = DateTime::createFromFormat('Y-m-d H:i:s', $end_time);
        }
        // add slots
        while ($t2 <= $stoptime) {
            $result[] = array('start' => $t1->format('Y-m-d H:i:s'), 'end' => $t2->format('Y-m-d H:i:s'));
            $t1->add($slotsize);
            $t2->add($slotsize);
        }
        if ($i < count($booked)) {
            // if booked slot was processed then resume from its end time
            $t1 = DateTime::createFromFormat('Y-m-d H:i:s', $booked[$i]['end']);
            $t2 = DateTime::createFromFormat('Y-m-d H:i:s', $booked[$i]['end'])->add($slotsize);
            $i++;
        } else {
            // we're done here
            break;
        }
    }
    var_dump($result);
    
    Login or Signup to reply.
  3. You can use DatePeriod combined with DateInterval to create ranges or slots to which you can then check if the slot intersects with any of the booked slots:

    /**
     * Create DatePeriod with start time and create end time by adding
     *  the booking interval then subtracting 1 second so that it doesn't
     *  overlap with next booking slot.
     * For example, if the booking length is 1 hour, it changes it to
     *  59 minutes, 59 seconds.
     */
    function createBookedDateTime(
        string $startDate,
        DateInterval $interval
    ): DatePeriod {
        $start = new DateTimeImmutable($startDate);
        return new DatePeriod(
            $start,
            $interval,
            $start->add($interval)->sub(new DateInterval('PT1S'))
        );
    }
    
    /**
     * Check if any of the DatePeriod values within the booking array's start and
     *  end dates overlap with the check time. If not, it is available for booking.
     */
    function isAvailable(array $bookingArray, DateTime $dtToCheck) {
        /** @var DatePeriod $bookedDt */
        foreach($bookingArray as $bookedDt) {
            if(
                $dtToCheck >= $bookedDt->getStartDate() &&
                $dtToCheck <= $bookedDt->getEndDate()
            ) {
                return false;
            }
        }
        return true;
    }
    
    // Length of booking
    $bookingLength = new DateInterval('PT1H');
    
    // DatePeriod array of already booked slots
    $booked = [
        createBookedDateTime('2022-10-21 12:00:00', $bookingLength),
        createBookedDateTime('2022-10-21 13:00:00', $bookingLength),
    ];
    
    // The range of time available for booking divided into slots by
    // the $bookingLength interval.
    $start = new DateTime('2022-10-21 09:00:00');
    $end = new DateTime('2022-10-21 15:00:00');
    $availability = new DatePeriod($start, $bookingLength, $end);
    
    // Array of available booking slots
    $availableToBook = array();
    
    // Loop to find available booking slots
    /** @var DateTime $available */
    foreach($availability as $available) {
    
        if(isAvailable($booked, $available)) {
            $availableToBook []= $available;
        }
    
    }
    
    var_dump($availableToBook);
    

    Here I have created a function which creates a DatePeriod for each of the booked slots by using the start date & time and adding the booking interval (minus 1 second) to set the end time. The 1 second is subtracted so that it doesn’t book the next time slot. For instance, if the slot is 1 hour long and 1:00 is booked that is 1:00 to 2:00. However, the next slot starts at 2:00 so 2:00 would not show as available. 1 second is removed making it 59 minutes and 59 seconds so the end is 1:59:59. The 2:00 slot is now available. Keep that in mind if you use it to display a booked slot’s length.

    Note that you may need to add a second or people may be confused why it shows 59 minutes and 59 seconds.

    The second function, isAvailable, is called within the loop to check if the time slot is booked or not. It compares the slot’s start time is between any of the start and end times of the booked slots. If so, it is already booked. If not, it is added to the $availableToBook array.

    You can change the booking length by altering $bookingLength to whatever you want (it is set to one hour). You can change it to 15 minutes (PT15M), 30 minutes (PT30M), 45 minutes (PT45M), or whatever time length you want.

    Login or Signup to reply.
  4. My approach was to essentially use the booked slots to split the given timeframe into available time periods. Available time slots are then allocated to the larger time periods separately. I believe this results in less loop iterations and should be better for performance (not that it matters much for small time frames).

    I split the functionality for determining the available time periods and allocating the available time slots into separate functions for better readability.

    This solution should be adaptive enough for the $slotLength DateInterval to be updated to accommodate all different slot lengths up to 24 hours. I know in your question that you addressed dates as string values, however I believe it’s much more side effect free and type safe to utilise PHP’s built in date related objects e.g. DateTimeImmutable, DateInterval where possible, therefore my solution follows this approach.

    <?php
    
    $timeframeStart = new DateTimeImmutable('2022-10-21 09:00:00');
    $timeframeEnd = new DateTimeImmutable('2022-10-21 15:00:00');
    
    $bookedSlots = [new DateTimeImmutable('2022-10-21 12:00:00'), new DateTimeImmutable('2022-10-21 13:00:00')];
    
    $slotLength = new DateInterval('PT1H'); // 1 Hour Interval
    
    $availableSlots = availableSlots($timeframeStart, $timeframeEnd, $bookedSlots, $slotLength);
    var_dump($availableSlots);
    
    function availableSlots(DateTimeImmutable $timeframeStart, DateTimeImmutable $timeframeEnd, array $bookedSlots, DateInterval $slotLength)
    {
        $availableSlots = [];
    
        $availableTimePeriods = availableTimePeriods($timeframeStart, $timeframeEnd, $bookedSlots, $slotLength);
    
        foreach ($availableTimePeriods as $availableTimePeriod) {
            $timePeriodMarker = $availableTimePeriod['timePeriodStart'];
            $timePeriodEnd = $availableTimePeriod['timePeriodEnd'];
    
            while ($timePeriodMarker < $timePeriodEnd) {
                $slotStart = $timePeriodMarker;
                $slotEnd = $slotStart->add($slotLength);
    
                if ($slotEnd <= $timePeriodEnd) {
                    $availableSlots[] = [
                        'start' => $slotStart,
                        'end' => $slotEnd,
                    ];
                }
    
                $timePeriodMarker = $slotEnd;
            }
        }
    
        return $availableSlots;
    }
    
    function availableTimePeriods(DateTimeImmutable $timeframeStart, DateTimeImmutable $timeframeEnd, array $bookedSlots, DateInterval $slotLength)
    {
        $amountOfBookedSlots = count($bookedSlots);
        $slotLengthInMinutes = $slotLength->h * 60 + $slotLength->i;
        $availableTimePeriods = [];
        $timeframeMarker = $timeframeStart;
    
        // Order booked slots based on time
        usort($bookedSlots, function ($dateA, $dateB) {
            return $dateA <=> $dateB;
        });
    
        for ($i = 0; $i < $amountOfBookedSlots; $i++) {
            $bookedSlot = $bookedSlots[$i];
            $isFirstBookedSlot = $i === 0;
            $isLastBookedSlot = $i === $amountOfBookedSlots - 1;
    
            // Include any available time periods before the booked slot
            if ($bookedSlot != $timeframeMarker) {
                $availableTimePeriods[] = [
                    'timePeriodStart' => $timeframeMarker,
                    'timePeriodEnd' => $bookedSlot,
                ];
    
                $timeframeMarker = $bookedSlot->add($slotLength);
            }
    
            if ($isLastBookedSlot) {
                // Include any available time period after the last booked slot
                if ($timeframeMarker != $timeframeEnd) {
                    $timeframeMarker = $bookedSlot->add($slotLength);
    
                    $availableTimePeriods[] = [
                        'timePeriodStart' => $timeframeMarker,
                        'timePeriodEnd' => $timeframeEnd,
                    ];
                }
            }
    
            // Time is booked up so skip this slot
            $timeframeMarker = $bookedSlot->add($slotLength);
        }
    
        // Return only time periods that at least one slot can fit into
        return array_filter($availableTimePeriods, function ($availableTimePeriod) use ($slotLengthInMinutes) {
            $duration = $availableTimePeriod['timePeriodEnd']->diff($availableTimePeriod['timePeriodStart']);
    
            $durationInMinutes = $duration->h * 60 + $duration->i;
    
            return $durationInMinutes >= $slotLengthInMinutes;
        });
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search