Calculating available timeslots in PHP
Solution:
The way I came up with to solve your problem is to convert your business hours and appointments arrays into a single availability array, which will contain the time slots at which you are available.
With this array, I can easily generate the array of slots you need by counting the number of slots available in a period of time, thanks to the provided slot duration (of 15 minutes in your example).
To be more clear, in your example, you have the following:
But I want to work with availabilities, so I need to convert {-code-2}
and {-code-3}
into an {-code-4}
array, which should be, in this example, the following:
{-code-4} = [
['09:00:00', '10:00:00'],
['10:30:00', '13:00:00'],
['14:00:00', '15:30:00'],
['16:15:00', '18:00:00']
];
Implementation
I am going to use Carbon for this example implementation. I am going to do a raw implementation of the functionality, then I will refactor into a proper class.
Getting breaks
The first thing I needed was an array of breaks. A break is a period during which you are not available. For instance, between 13h00 and 14h00 because it's a business hour pause, and between 10h00 and 10h30 because of an appointment.
So, to put it simply, the breaks are a mapping of the last item of each {-code-2}
's entry and the first item of the next one, as well as every {-code-3}
entry.
Because of this, I need to create a function that returns such mapping. Here is that function:
{-code-8}
This is not a good code, but it gets the work done (and I'll refactor later). This function loops through the given array, takes it last value and the first value of the next one, and add them in another array.
Additionally, it keeps track of the first element and the last element.
Using this function on {-code-2}
, I get the following array:
{-code-10}
Which is exactly what I want. Combined to {-code-3}
, I now have all of the breaks:
{-code-12}
You can notice how they are not in order. To fix that, I use to sort by the first hour in each array:
{-code-13}({-code-18}, fn ($a, $b) => $a[0] {-code-15} $b[0]);
Note: if you didn't know about it,
{-code-15}
is the spaceship operator.
Getting the availabilities
Now that we have the breaks, we can use the same pattern we used on {-code-2}
to get your availabilities.
Because you are available when you are not in break, I just have to reuse the {-code-17}
function on {-code-18}
to get a mapping of the periods during which you are not in a pause nor at an appointment.
Earlier, I used it on {-code-2}
like the following:
[{-code-18}, $startOfDay, $endOfDay] = {-code-17}({-code-2});
Now, I will use it on {-code-18}
like the following:
[{-code-4}, $first, $last] = {-code-17}({-code-18});
Thanks to these two calls, I can have a complete {-code-4}
array:
{-code-4} = [
[$startOfDay, $first],
...{-code-4},
[$last, $endOfDay]
];
Generating availability slots from a period
Now that we have what we needed, we need to think about the initial problematic: you need to get an array of slots. Earlier, I explained that you could generate it thanks to the slot duration, which is, in this case, of 15 minutes.
To do this, I will create a {-code-25}
function:
function {-code-25}($start, $end) {
$slotDuration = 15;
$start = Carbon::createFromTimeString($start);
$end = Carbon::createFromTimeString($end);
$slotCount = (int) ($start->diffInMinutes($end) / $slotDuration);
$slots = [];
for ($i = 0; $i < $slotCount; $i++) {
$slots[] = [
$start->format('H:i:s'),
$start->addMinutes($slotDuration)->format('H:i:s')
];
}
return $slots;
}
The interesting part is how I get to know the amount of slots for the given starting time, by getting the difference, in minutes, between the ending time and the starting time, and by dividing that amount by the duration of the slot.
For instance, if you are free from 14:00:00 to 16:00:00, the difference in minutes between the two will be 120. Divided by 15, we know that you have 8 slots of 15 minutes of availability.
Next thing, we loop 8 times to add these periods to an array.
Getting the slots from the availabilities
Last thing we need to do is to call {-code-25}
for every availability period. This is quite simple:
function generate_slot_array_from_availability_array({-code-4}) {
$slots = [];
foreach ({-code-4} as [$start, $end]) {
$slots = array_merge(
$slots,
{-code-25}($start, $end)
);
}
return $slots;
}
This is it. When called with the {-code-4}
array, this function will return exactly what you wanted: an array of slots, excluding the business hour breaks and the appointments.
generate_slot_array_from_availability_array({-code-4});
Full code
Procedural
Here is the full procedural code, the first one that I implemented to get the functionality ready: https://phpsandbox.io/e/x/jolly-sun-d875
Object Oriented
Here is the full, cleaned up, object-oriented, fluent code that I refactorised from the procedural one: https://phpsandbox.io/e/x/quiet-snow-x2xq
{-code-31}
<?php
namespace Calendar;
use Carbon\Carbon;
class Calendar
{
protected array {-code-2};
protected array {-code-3};
protected array {-code-4};
public function __construct()
{
$this->businessHours = [];
$this->appointments = [];
$this->availability = [];
}
/*
|Answer
Answer
Answer
Answer
Answer
Answer
Answer
----
| Fluent API
|Answer
Answer
Answer
Answer
Answer
Answer
Answer
----
*/
public static function create(): self
{
return new static();
}
public function withBusinessHours(array {-code-2}): self
{
$this->businessHours = {-code-2};
return $this;
}
public function withAppointments(array {-code-3}): self
{
$this->appointments = {-code-3};
return $this;
}
/**
* Gets the available slots.
*/
public function getSlots(int $duration = 15): array
{
$slots = [];
foreach ($this->getAvailabilityArray() as [$start, $end]) {
$slots = array_merge(
$slots,
$this->getSlotsFromPeriod(
Carbon::createFromTimeString($start),
Carbon::createFromTimeString($end),
$duration)
);
}
return $slots;
}
/*
|Answer
Answer
Answer
Answer
Answer
Answer
Answer
----
| Helpers
|Answer
Answer
Answer
Answer
Answer
Answer
Answer
----
*/
protected function formatDate(Carbon $date, string $format = 'H:i:s'): string
{
return $date->format($format);
}
/**
* Maps the end of each item to the start of the next one.
*
* @return array Returns an array which contains, in order, the new mapped array,
*/
protected function mapArrayEndToStart($array): array
{
$result = [];
$itemCount = count($array);
for ($i = 0; $i < $itemCount; ++$i) {
$isLast = $i === $itemCount - 1;
if ($isLast) {
continue;
}
$result[] = [
$array[$i][1],
$array[$i + 1][0],
];
}
return $result;
}
/**
* Gets an array containing the availability slots.
*/
protected function getAvailabilityArray(): array
{
$breaks = [
...$this->mapArrayEndToStart($this->businessHours),
...$this->appointments,
];
usort($breaks, fn ($a, $b) => $a[0] <=> $b[0]);
$availability = [
[
$this->businessHours[0][0],
$breaks[0][0],
],
...$this->mapArrayEndToStart($breaks),
[
$breaks[count($breaks) - 1][1],
$this->businessHours[count($this->businessHours) - 1][1],
],
];
return $availability;
}
/**
* Gets an array of slots of the format [slotStartTime, slotEndTime] for the given time period.
*/
protected function getSlotsFromPeriod(Carbon $start, Carbon $end, int $slotDuration = 15): array
{
$count = (int) ($start->diffInMinutes($end) / $slotDuration);
$slots = [];
for ($i = 0; $i < $count; ++$i) {
$slots[] = [
$this->formatDate($start),
$this->formatDate($start->addMinutes($slotDuration)),
];
}
return $slots;
}
}
{-code-33}
{-code-34}