Imagine you want to reserve a classroom over several days. You get a schedule, from the school, telling you when a certain classroom is still free. To make it easy for your students, you want to reserve the classroom at the same time every day. Regrettably the schedule is long and complex, and you cannot figure out when the classroom will be free on all the days you need it.
The code below tries to solve that problem. It finds free time slots that are common to all dates, and their given free time slots.
It divides the day in 96 quarters of an hour (4 x 24), and creates an array for them. This is called a 'timeline'. Then the time slots, for each date, are matched against a blank timeline, creating a timeline for those dates. The special trick is combining these timelines in timeline_intersect()
, effectively doing an AND operation on all array values. Finally it turns the timeline back into time slots.
This code does return the expected result.
<?php
// input data
$openTimeslots = ['02-09-2021' => [ '8:00-10:00', '16:00-19:00'],
'03-09-2021' => [ '7:00-10:00', '16:15-19:00',
'14:00-16:00', '13:00-14:15'],
'04-09-2021' => [ '7:15-10:00', '15:15-18:15']];
const QUARTER_FREE = 'free'; // unused
const QUARTER_USED = 'used'; // this quarter falls within a time slot
function newTimeline($status = QUARTER_FREE)
{
$timeline = [];
for ($quarter = 0; $quarter < 4 * 24; $quarter++) {
$timeline[$quarter] = $status;
}
return $timeline;
}
function toTime($quarter)
{
return sprintf('%02d', intdiv($quarter, 4)) . ':' .
sprintf('%02d', (($quarter % 4) * 15));
}
function toQuarters($time)
{
list($hours, $minutes) = explode(':', $time);
return (4 * $hours) + intdiv($minutes, 15);
}
function fillTimelineWithTimeslot($timeline, $timeslot)
{
list($startTime, $finishTime) = explode('-', $timeslot);
for ($quarter = toQuarters($startTime); $quarter < toQuarters($finishTime); $quarter++) {
$timeline[$quarter] = QUARTER_USED;
}
return $timeline;
}
function extractTimeslotsFromTimeline($timeline)
{
$timeslots = [];
$inUse = FALSE;
foreach ($timeline as $quarter => $usage) {
if ($inUse && ($usage == QUARTER_FREE)) {
$timeslots[] = toTime($startQuarter) . '-' . toTime($quarter);
$inUse = FALSE;
}
elseif (!$inUse && ($usage == QUARTER_USED)) {
$startQuarter = $quarter;
$inUse = TRUE;
}
}
return $timeslots;
}
function timeline_intersect($timeline1, $timeline2)
{
foreach ($timeline2 as $quarter => $usage) {
$bothUsed = ($timeline1[$quarter] == QUARTER_USED) &&
($usage == QUARTER_USED);
$timeline1[$quarter] = $bothUsed ? QUARTER_USED : QUARTER_FREE;
}
return $timeline1;
}
// start of algorithm
$combinedTimeline = newTimeline(QUARTER_USED);
foreach ($openTimeslots as $date => $timeslots) {
$timeline = newTimeline();
foreach ($timeslots as $timeslot) {
$timeline = fillTimelineWithTimeslot($timeline, $timeslot);
}
$combinedTimeline = timeline_intersect($combinedTimeline , $timeline);
}
var_export(extractTimeslotsFromTimeline($combinedTimeline));
Here is a PHP fiddle. The result is:
array (
0 => '08:00-10:00',
1 => '16:15-18:15'
)
If required this code could easily be converted to work with minutes, instead of quarters of hours, but since the time slots are given in quarters of hours this is, I think, the most efficient way to do this.
The requirements are:
- Time slot are set in whole quarters of hours. Only valid slots are given.
- Should be able to work with many dates.
- Should be able to work with many time slots.
My questions are:
- Is there a more efficient, more optimized, way to do this?
- Did I miss any bugs? (I hope not!)
- I used simple functions. Would OOP do a better job? If so, why? And how?
Have some fun playing with this code, and thank you for reading this.
$openTimeslots
at the top of the snippet, but where is the declaration of what is used? Why does the input have 3 days listed, but the output loses the date relationship? I think I am misunderstanding the purpose of your script. As an aside, please use 3v4l.org as your demo sandbox because sandbox.onlinephpfunctions.com is positively rank on my mobile device. \$\endgroup\$fillTimelineWithTimeslot()
. I then process only timelines, combining all the timelines into one withtimeline_intersect()
, and only at the end I convert the one timeline back to slots withextractTimeslotsFromTimeline()
. Yes, I think you might have misunderstood the purpose. No problem. The PHP Fiddle was only supplied for your convenience, you're free to execute the code any other way you can. \$\endgroup\$