I am working on a feature that checks if external events collide with internal events (in a calendar app). The process looks like this:
$internalEvents
)$externalEvents
). These are existing events with the type busy
.I tried to reduce it to the minimum as much as possible. This is the data input:
$internalEvents = array(
array(
"start" => "03/29/2016 12:00:00",
"end" => "03/29/2016 13:00:00"
),
array(
"start" => "03/29/2016 12:30:00",
"end" => "03/29/2016 13:30:00"
),
array(
"start" => "03/29/2016 13:00:00",
"end" => "03/29/2016 14:00:00"
),
array(
"start" => "03/29/2016 13:30:00",
"end" => "03/29/2016 14:50:00"
),
array(
"start" => "03/29/2016 14:00:00",
"end" => "03/29/2016 15:00:00"
),
array(
"start" => "03/29/2016 14:30:00",
"end" => "03/29/2016 15:30:00"
),
array(
"start" => "03/29/2016 15:00:00",
"end" => "03/29/2016 16:00:00"
),
array(
"start" => "03/29/2016 15:30:00",
"end" => "03/29/2016 16:30:00"
),
array(
"start" => "03/29/2016 16:00:00",
"end" => "03/29/2016 17:00:00"
)
);
$externalEvents = array(
array(
"start" => "03/29/2016 08:00:00",
"end" => "03/29/2016 12:00:00",
"type" => "busy"
),
array(
"start" => "03/29/2016 15:30:00",
"end" => "03/29/2016 16:00:00",
"type" => "busy"
),
array(
"start" => "03/29/2016 13:30:00",
"end" => "03/29/2016 14:15:00",
"type" => "busy"
)
);
Now I try to find any kind of conflict by comparing the internal event to all external events:
foreach($internalEvents as $internalEvent) {
$internalEventStart = new DateTime($internalEvent['start']);
$internalEventEnd = new DateTime($internalEvent['end']);
$result = true;
echo "\nverifying " . $internalEventStart->format('Y-m-d H:i') . " - " . $internalEventEnd->format('Y-m-d H:i') . "\n";
foreach($externalEvents as $externalEvent) {
$externalEventStart = new DateTime($externalEvent['start']);
$externalEventEnd = new DateTime($externalEvent['end']);
// check if there are conflicts between internal and external events
if ($internalEventStart >= $externalEventStart && $internalEventStart <= $externalEventEnd) {
$result = false;
echo " problem 1: event is between busy time: " . "\n";
}
if ($internalEventStart >= $externalEventStart && $internalEventStart <= $externalEventEnd && $externalEventEnd <= $internalEventEnd) {
$result = false;
echo " problem 2: event starts during busy time: " . "\n";
}
if ($internalEventStart <= $externalEventStart && $externalEventStart <= $internalEventEnd && $internalEventEnd <= $externalEventEnd) {
$result = false;
echo " problem 3: event stops during busy time: " . "\n";
}
if (($internalEventStart <= $externalEventStart) && ($externalEventStart <= $externalEventEnd) && ($externalEventEnd <= $internalEventEnd)) {
$result = false;
echo " problem 4: event during busy time: " . "\n";
}
if (($internalEventStart <= $internalEventEnd) && ($internalEventEnd <= $externalEventStart) && ($externalEventStart <= $externalEventEnd)) {
$result = false;
echo " problem 5: event during busy time: " . "\n";
}
}
if($result) {
echo " result: OK\n";
} else {
echo " result: NOT OK \n";
}
}
I am looking for an algorithm that can find any possible event overlap conflict. Any hint is highly appreciated.
The running code can be found here (IDEone.com).
When two events collide? See this schema:
----E---- CS/EE CE/ES
--N-- < <
--N-- > >
--C-- < >
--C-- < >
--C-- < >
-----C----- < >
··················································
E = Main Event
N = Not Collide Event
C = Collide Event
CS = Compare Event Start
EE = Main Event End
CE = Compare Event End
ES = Main Event Start
As you can see, there is collision only when the start of event C is before the end of event E and the end of event E is after the start of event C. Knowing this helps to find an efficient and short method to compare events.
About the code, a preliminary note: you convert the dates in ISO 8601 before comparing it multiple times in your code, so why do not create a function for this?
function eventsToDate( $row )
{
$retval = array( 'start' => date_create( $row['start'] )->format('Y-m-d H:i:s'), 'end' => date_create( $row['end'] )->format('Y-m-d H:i:s') );
$retval['print'] = sprintf( '%s-%s', substr( $retval['start'],-8,5 ), substr( $retval['end'],-8,5 ) );
return $retval;
}
This function returns an associative array with 'start' and 'end' in your format. I have added a third key, 'print', to use during debug. Note that in the print I consider only hours:minutes (all the dates in your array sample are from same day), but the comparison is made on complete dates. You can omit this 'print' key or replace it with preferred output format.
You execute two nested foreach
, and for each loop recalculate the date format. With your array samples, you call DateTime/DateTime::format 36 times. By creating a temporary array with all converted $externalEvents
, we can reduce these calls to 12. So, before starting foreach()
loop, we use array_map
with above custom function and $externalEvents
array to create an array with formatted dates:
$externalDates = array_map( 'eventsToDate', $externalEvents );
Then, we start the main foreach()
loop on $internalEvents
:
foreach( $internalEvents as $internalEvent )
{
$internalDates = eventsToDate( $internalEvent );
echo $internalDates['print'] . PHP_EOL;
$result = True;
foreach( $externalDates as $externalDate )
{
At this point, we compare dates. As mentioned above, we compare start with end and end with start. To simplify next comparison, we use strcmp
, a php function that “returns < 0 if str1 is less than str2, > 0 if str1 is greater than str2, and 0 if they are equal”:
$startCmp = strcmp( $internalDates['start'], $externalDate['end'] );
$endCmp = strcmp( $internalDates['end'], $externalDate['start'] );
Now, the comparison:
if( $startCmp<0 && $endCmp>0 )
{
$result = False;
echo " {$externalDate['print']} COLLIDE\n";
}
else
{
echo " {$externalDate['print']} OK\n";
}
}
And, finally, we can print the result:
echo "Result: " . ( $result ? 'OK' : 'NOT OK') . "\n\n";
}
eval.in demo
Note: with above comparison, with first $internalEvent
we obtain following result:
12:00-13:00
08:00-12:00 OK
15:30-16:00 OK
13:30-14:15 OK
Result: OK
Instead, if you want this result:
12:00-13:00
08:00-12:00 COLLIDE
15:30-16:00 OK
13:30-14:15 OK
Result: NOT OK
You have to replace above if
condition with this:
if( $startCmp<=0 && $endCmp>=0 )
Above code will work, if you want more details about kind of collision, you can test other combinations of start/end inside if
condition.
Alternative: Return Colliding Events
If — instead of printing results — you want catch colliding events, you can replace nested foreach()
with array_filter
in this way:
$result = array_filter
(
$externalDates,
function( $row ) use( $internalDates )
{
$startCmp = strcmp( $internalDates['start'], $row['end'] );
$endCmp = strcmp( $internalDates['end'], $row['start'] );
return( $startCmp<0 && $endCmp>0 );
}
);
At this point, colliding events are in array $result
. Obviously, is the array is empty, there are not collisions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With