We are looking to create a list of comma separated dates that tell us when a reservation is full. There are 7 units to rent so we want to know which dates are present >= 7
This Stackoverflow thread is close as it identifies intersections but I am looking for the specific dates where they intersect x amount of times.
<?php
// 2019-2-21 is present 8 times in the following array
$ranges = array(
array('id' =>'59','start' => new DateTime('2019-02-19'), 'end' => new DateTime('2019-02-21')),
array('id' =>'58','start' => new DateTime('2019-02-19'), 'end' => new DateTime('2019-02-21')),
array('id' =>'55','start' => new DateTime('2019-02-19'), 'end' => new DateTime('2019-02-21')),
array('id' =>'57','start' => new DateTime('2019-02-19'), 'end' => new DateTime('2019-02-21')),
array('id' =>'108','start' => new DateTime('2019-02-21'), 'end' => new DateTime('2019-02-28')),
array('id' =>'109','start' => new DateTime('2019-02-19'), 'end' => new DateTime('2019-02-24')),
array('id' =>'110','start' => new DateTime('2019-02-21'), 'end' => new DateTime('2019-02-23')),
array('id' =>'111','start' => new DateTime('2019-02-21'), 'end' => new DateTime('2019-02-25')),
);
function intersects($lhs, $rhs) {
return !($lhs['start'] > $rhs['end'] || $lhs['end'] < $rhs['start']);
}
function checkDates($ranges) {
// Comparison loop
for($i = 0; $i < sizeof($ranges); $i++) {
for($j = $i+1; $j < sizeof($ranges); $j++) {
if(intersects($ranges[$i], $ranges[$j])) {
echo "Date {$i} intersects with date {$j}<br>";
}
}
}
}
checkDates($ranges);
?>
I'm able to identify on a known specific date when we are over the limit
SELECT COUNT(*) FROM reservations
WHERE reservations.`date` <= '$date' AND reservations.`dateLast` >= '$date'
This gives us a count that we can compare to our qty of units but I'm not sure how to create a list of dates that intersect >= x so we can know in advance if we are sold out.
UPDATE to confirm solution:
foreach ($ranges as $range) {
while ($range['start'] <= $range['end']) {
$date = $range['start']->format('Y-m-d');
$dates[$date] = (isset($dates[$date]) ? $dates[$date] : 0) + 1; 1;//define new $dates array
$range['start']->modify('+1 day');
}
}
echo $sold_out = array_filter($dates, function($n) { return $n >= 7; });
echo '<pre>';
print_r($range);
echo '</pre>';
I think you don't need to intersect the ranges. You just need to know how many times each date appears in your list of ranges, so you can just iterate each range in ranges and count the dates.
foreach ($ranges as $range) {
while ($range['start'] <= $range['end']) {
$date = $range['start']->format('Y-m-d');
$dates[$date] = ($dates[$date] ?? 0) + 1;
// or $dates[$date] = (isset($dates[$date]) ? $dates[$date] : 0) + 1;
$range['start']->modify('+1 day');
}
}
/* Result:
array (size=10)
'2019-02-19' => int 5
'2019-02-20' => int 5
'2019-02-21' => int 8
'2019-02-22' => int 4 ...
*/
Then you can filter that to find any sold out dates.
$sold_out = array_filter($dates, function($n) { return $n >= 7; });
I think you can probably also do this in SQL by creating a temporary table with all dates in the date range you're interested in and joining it to your count query.
I am trying to write a script which will display either an open or closed message depending on a businesses operating hours. I tried using the solution found here and adding to this by setting the timezone. However when I try to run this the value of $status is always 'closed' even if the time of day falls within the values in the array.
Here is my code, any advice would be appreciated. Thanks
//setting default timezone
date_default_timezone_set('Europe/Dublin');
//creating array of opening hours
$openingHours = [
'Sun' => ['12:00' => '20:30'],
'Wed' => ['16:00' => '21:00'],
'Thu' => ['16:00' => '21:00'],
'Fri' => ['16:00' => '23:00'],
'Sat' => ['16:00' => '21:00']
];
//current timestamp
$timestamp = time();
//default status
$status = 'closed';
//get time object from timestamp
$currentTime = (new DateTime())->setTimestamp($timestamp);
//loop through time range for current day
foreach ($openingHours[date('D', $timestamp)] as $startTime => $endTime) {
//create time objects from start and end times
$startTime = DateTime::createFromFormat('h:i A', $startTime);
$endTime = DateTime::createFromFormat('h:i A', $endTime);
//check if current time is within range
if (($startTime < $currentTime) && ($currentTime < $endTime)) {
$status = 'open';
break;
}//end off if
}//end off foreach
echo "we are currently :$status";
In your code the currentTime variable is an object so when you try comparing it things aren't going to match. Try this instead which simplifies things a little.
date_default_timezone_set('Europe/Dublin');
//creating array of opening hours
$openingHours = [
'Sun' => ['12:00' => '20:30'],
'Wed' => ['16:00' => '21:00'],
'Thu' => ['16:00' => '21:00'],
'Fri' => ['16:00' => '23:00'],
'Sat' => ['16:00' => '21:00']
];
//default status
$status = 'closed';
$timeNow = date('H:m',time());
//loop through time range for current day
foreach ($openingHours[date('D', time())] as $startTime => $endTime) {
//check if current time is within range
if (($startTime < $timeNow) && ($timeNow < $endTime)) {
$status = 'open';
break;
} //end off if
} //end off foreach
echo "we are currently :$status";
Edit on July 7th:
If you can change you the opening hours array slightly you may avoid multiple calls to date and the foreach loop using the code below.
date_default_timezone_set('Europe/Dublin');
//creating array of opening hours
$openingHours = [
'Sun' => ['Open'=>'12:00','Close' => '20:30'],
'Wed' => ['Open'=>'16:00','Close' => '21:00'],
'Thu' => ['Open'=>'16:00','Close' => '21:00'],
'Fri' => ['Open'=>'16:00','Close' => '23:00'],
'Sat' => ['Open'=>'16:00','Close' => '21:00']
];
$timeNow = date('H:m',time());
$dow = date('D',time());
echo 'We are currently :' . ((($openingHours[$dow]['Open'] < $timeNow) AND
($timeNow < $openingHours[$dow]['Close'])) ? 'open' : 'closed');
I have array like this:
$events = [
[
'title' => 'Event 1',
'description' => 'Some Text',
'date_added' => 2016-05-06 14:57:39
],
[
'title' => 'Event 2',
'description' => 'Some Text',
'date_added' => 2016-05-08 14:57:39
],
[
'title' => 'Event 3',
'description' => 'Some Text',
'date_added' => 2016-05-09 14:57:39
],
[
'title' => 'Event 4',
'description' => 'Some Text',
'date_added' => 2016-05-09 15:57:39
]
];
I need to show data like this:
**Today events:**
Event 4,
Event 3
**Yesterday events**
Event 2
**May 6th events**
Event 1
How can I do this? Is it possible?
This works exactly as you want. Try this:
First, we need to sort the records date wise. We can use usort() function for this.
function compare_date($a, $b) {
if ($a['date_added'] === $b['date_added']){
return 0;
}
return ($a['date_added'] > $b['date_added']) ? -1 : 1;
}
usort($events, "compare_date");
Next, we're creating a custom array that holds titles date wise.
foreach($events as $event) {
$today_date = strtotime(date("Y-m-d H:i:s"));
$event_date = strtotime($event['date_added']);
$diff = floor(($today_date - $event_date)/(60*60*24));
switch($diff) {
case 0:
$title = "Today events:<br/>";
$result[$title][] = $event['title'];
break;
case 1:
$title = "Yesterday events:<br/>";
$result[$title][] = $event['title'];
break;
default:
$title = date("M jS", $event_date)." Events:<br/>";
$result[$title][] = $event['title'];
}
}
Finally, we are traversing through this $result and showing the desired output.
foreach($result as $k => $rslt) {
echo $k;
echo implode("<br>", $rslt);
echo "<br/>";
}
You can do this using usort():
function date_compare($a, $b) {
$t1 = strtotime($a['date_added']);
$t2 = strtotime($b['date_added']);
return $t1 - $t2;
}
usort($events, 'date_compare');
To display them, you can use the following foreach statement:
foreach ($events as $key) {
$dt = new DateTime($key['date_added']);
$date = $dt->format('F j');
if($current != $date) {
$current = $date;
echo "<br>**".$date." events:**<br>";
}
echo $key["title"]."<br>";
}
The output:
**May 1 events:**
Event 3
**May 3 events:**
Event 1
**May 8 events:**
Event 2
**May 9 events:**
Event 4
Hope this solution works. Wish you the best.
You can also try this :
// method to compare two values
function compare_date($a, $b) {
if ($a['date_added'] === $b['date_added']){
return 0;
}
return ($a['date_added'] > $b['date_added']) ? -1 : 1;
}
// callback function
usort($events, "compare_date");
$today = date("Y-m-d");
$yesterday = date('Y-m-d', strtotime("yesterday"));
$myEvents = array();
foreach ($events as $key => $val) :
$datetime = new DateTime($val['date_added']);
$record_date = $datetime->format('Y-m-d');
if ($today == $record_date):
$myEvents['today'][] = $events[$key];
elseif ($yesterday == $record_date):
$myEvents['yesterday'][] = $events[$key];
else :
$myEvents[$record_date][] = $events[$key];
endif;
endforeach;
// display event
foreach($myEvents as $i => $v):
if ($i!="today" && $i!="yesterday"):
echo date('M d', strtotime($i)) . "<br>";
else :
echo $i . "<br>";
endif;
foreach ($v as $e):
echo $e['title'] . "<br>";
endforeach;
echo "<br>";
endforeach;
Out put of above code is :
today
Event 4
Event 3
yesterday
Event 2
May 06
Event 1
function checkDateOverlap($ranges) {
$res = $ranges[0];
$countRanges = count($ranges);
for ($i = 0; $i < $countRanges; $i++) {
$r1s = $res['start'];
$r1e = $res['end'];
$r2s = $ranges[$i]['start'];
$r2e = $ranges[$i]['end'];
if ($r1s >= $r2s && $r1s <= $r2e || $r1e >= $r2s && $r1e <= $r2e || $r2s >= $r1s && $r2s <= $r1e || $r2e >= $r1s && $r2e <= $r1e) {
$res = array(
'start' => $r1s > $r2s ? $r1s : $r2s,
'end' => $r1e < $r2e ? $r1e : $r2e
);
} else
return false;
}
return $res;
}
// example of returned dates that overlap
$ranges = array(
array('start' => '2014-01-01', 'end' => '2014-01-04'),
array('start' => '2014-01-05', 'end' => '2014-01-10'),
array('start' => '2014-01-04', 'end' => '2014-01-07')
);
//example of failure
$ranges2 = array(
array('start' => '2014-01-01', 'end' => '2014-01-04'),
array('start' => '2014-01-05', 'end' => '2014-01-10'),
array('start' => '2014-01-11', 'end' => '2014-01-17')
);
var_dump(checkDateOverlap($ranges));
The following is what I was attempting to check intersection of date ranges. In the array "ranges1" this example has overlapping dates. It should return the dates. In array $ranges2, this should pass as no intersecting dates.
Now the weird thing is the start and end date can be the exact same, so you could make an entry for just a single day. I've tried many things, and I'm stumped.
I believe there needs to be another for loop, but regardless I am not getting success.
Here was another go I had:
<?php
// pass your ranges to this method and if there is a common intersecion it will
// return it or false
function checkDateOverlap($ranges){
$res = $ranges[0];
$countRanges = count($ranges);
for ($i = 0; $i < count($countRanges); $i++) {
for($j = $i+1; $j < count($countRanges); $j++) {
$r1s = $res['start'];
$r1e = $res['end'];
$r2s = $ranges[$i]['start'];
$r2e = $ranges[$i]['end'];
if (($r1s >= $r2e && $r2s <= $r1e)) {
$res[] = array(
'start' => $r1s > $r2s ? $r1s : $r2s,
'end' => $r1e < $r2e ? $r1e : $r2e
);
} else
return false;
}
}
return $res;
}
// example
$ranges = array(
array('start' => '2014-01-04', 'end' => '2014-01-05'),
array('start' => '2014-01-06', 'end' => '2014-01-10'),
array('start' => '2014-01-11', 'end' => '2014-01-13')
);
echo "<pre>";
var_dump(checkDateOverlap($ranges));
echo "</pre>";
Any advice greatly appreciated.
$ranges = array(
array('start' => new DateTime('2014-01-01'), 'end' => new DateTime('2014-01-05')),
array('start' => new DateTime('2014-01-06'), 'end' => new DateTime('2014-01-06')),
array('start' => new DateTime('2014-01-07'), 'end' => new DateTime('2014-01-07')),
);
function intersects($lhs, $rhs) {
// Note that this function allows ranges that "touch",
// eg. one pair starts at the exact same time that the other ends.
// Adding less "or equal to" will allow same start date
return !($lhs['start'] > $rhs['end'] || $lhs['end'] < $rhs['start']);
}
function checkDates($ranges) {
// Comparison loop is of size n•log(n), not doing any redundant comparisons
for($i = 0; $i < sizeof($ranges); $i++) {
for($j = $i+1; $j < sizeof($ranges); $j++) {
if(intersects($ranges[$i], $ranges[$j])) {
echo "Date {$i} intersects with date {$j}\n";
}
}
}
}
checkDates($ranges);
I've attached my working code sample to hopefully help someone else in the future looking for the same solution. This will print the arrays that intersect.
If you use usort to first sort the dates, the work gets a lot easier. The following can be optimized a lot, but it is done step-by-step make it easier to understand.
//The date comparison function, sort on start and then on end
function cmp($a, $b)
{
if($a['start']<$b['start']) return -1;
if($a['start']>$b['start']) return 1;
if($a['end']<$b['end']) return -1;
if($a['end']>$b['end']) return 1;
return 0; // start=start and end=end
}
$ranges = array(
array('start' => '2014-01-01', 'end' => '2014-01-04'),
array('start' => '2014-01-05', 'end' => '2014-01-10'),
array('start' => '2014-01-04', 'end' => '2014-01-07')
);
usort($ranges, 'cmp'); // Sort the dates
$output = array();
for($i=0; $i<sizeof($ranges); $i++)
{
$endindex = $i; // The index containing the proper 'end' value
for($j=$i+1; $j<sizeof($ranges); $j++)
{
if($ranges[$endindex]['start'] == $ranges[$j]['start']) // Overlap
$endindex = $j;
elseif($ranges[$endindex]['end']>=$ranges[$j]['start']) // Overlap
$endindex = $j;
}
$output[] = array('start' => $ranges[$i]['start'], 'end' => $ranges[$endindex]['end']);
// Break the rules by hard-setting $i from the for loop - it works great in this case
$i = $endindex;
}
print_r($output);
It works for your example. If you have other rules that must be used, hopefully you can adjust this code.
Here are some remarks:
- You do not check the validity of the date formed by 'start' and 'end'.
- Why do you not convert the dates to timestamp ?
-> It's more easier and faster to compare integer value instead of string ?
Why do you not use PHP DateTime and DateInterval Objects ?
http://php.net/manual/en/book.datetime.php
I have a function that take 2 arrays ($schedule, $remove), both are arrays of days with time inside, it will remove time from the schedule .
Now this function is working fine if I have between 1 & 20 user it takes 2-4 seconds to generate the calendar which is fine but when having 20+ user with a lot of schedules entries it goes to 15+ seconds.
I'm working with CodeIgniter and I have this function in a helper where it's called a lot.
So I wanted to know if you guys can see any better way to deal with my problem or adjustments that I make to my algorithm to make it faster.
Note:
In my code below, the big problem I see is the recursive call and the break of the loop every time I modify the structure.
I loop on both arrays and do test to see if the absence is inside/overlap/equal/outside of the availability and then recall the function if the structure was modified if not return the final structure.
Note 2 :
On local the Apache crash because the recursive function sometime is called more than 100 times .
Here is the code I have :
function removeSessionsFromSchedule($schedule, $remove) {
$modified = false;
if (is_array($schedule) && count($schedule) > 0 && is_array($remove) && count($remove) > 0 && checkArrayEmpty($remove)) {
// Minimise the iterations
$remove = minimiseRemoveSchedule($remove);
foreach ($schedule as $s => $dispo) {
if ($modified) {
break;
}
$pos = 0;
$countdispo = count($dispo);
foreach ($dispo as $d) {
$abs = isset($remove[$s]) ? $remove[$s] :null;
$counter = 0;
// availability start/end
$dis_s = strtotime($d['heure_debut']);
$dis_e = strtotime($d['heure_fin']);
if (is_array($abs) && count($abs) > 0) {
foreach ($abs as $a) {
// absence start/end
$abs_s = strtotime($a['heure_debut']);
$abs_e = strtotime($a['heure_fin']);
// Tests to see the if there is overlap between absence and availability
// (2) [a_s]---[ds - de]---[a_e]
if ($abs_s <= $dis_s && $abs_e >= $dis_e) {
// delete availability
unset($schedule[$s][$pos]);
$modified = true;
break;
}
// (7)[as == ds] && [ae < de]
else if ($abs_s == $dis_s && $abs_e < $dis_e) {
unset($schedule[$s][$pos]);
$schedule[$s][$pos] = $d;
$schedule[$s][$pos]['heure_debut'] = date("H:i", $abs_e);
$schedule[$s][$pos]['heure_fin'] = date("H:i", $dis_e);
$modified = true;
break;
}
// (6) [ds -de] --- [as ae] return dispo as is
else if ($abs_s >= $dis_e) {
unset($schedule[$s][$pos]);
$schedule[$s][$pos] = $d;
$modified ?: false;
}
// (5)[as ae] [ds -de] --- return dispo as is
else if ($abs_e <= $dis_s) {
unset($schedule[$s][$pos]);
$schedule[$s][$pos] = $d;
$modified ?: false;
}
// (1)[ds] --- [as] --- [ae] --- [de] (duplicate dis with new times)
else if ($abs_s > $dis_s && $abs_e <= $dis_e) {
// new times as : // s1 = ds-as && s2 = ae-de
unset($schedule[$s][$pos]);
$schedule[$s][$pos] = $d;
$schedule[$s][$pos + 1] = $d;
$schedule[$s][$pos]['heure_debut'] = date("H:i", $dis_s);
$schedule[$s][$pos]['heure_fin'] = date("H:i", $abs_s);
$schedule[$s][$pos + 1]['heure_debut'] = date("H:i", $abs_e);
$schedule[$s][$pos + 1]['heure_fin'] = date("H:i", $dis_e);
// a revoir si ca ne cause pas d'autre problem qu'on fasse pos++ ...
$pos++;
$modified = true;
break;
}
// (3)[as] -- [ds] --- [ae] -- [de]
else if ($abs_s < $dis_s && $abs_e < $dis_e) {
unset($schedule[$s][$pos]);
$schedule[$s][$pos] = $d;
$schedule[$s][$pos]['heure_debut'] = date("H:i", $abs_e);
$schedule[$s][$pos]['heure_fin'] = date("H:i", $dis_e);
$modified = true;
break;
}
// (4) [ds]---[as]--- [de]--- [ae]
else if ($abs_s > $dis_s && $abs_s < $dis_e && $abs_e > $dis_e) {
unset($schedule[$s][$pos]);
$schedule[$s][$pos] = $d;
$schedule[$s][$pos]['heure_debut'] = date("H:i", $dis_s);
$schedule[$s][$pos]['heure_fin'] = date("H:i", $abs_s);
$modified = true;
break;
} else {
$modified ?: false;
}
}
// if($modified == true) { break;}
} else {
$modified = false;
}
$pos++;
}
}
} else {
$modified = false;
}
if ($modified) {
$schedule = resetIndexes($schedule);
$schedule = sortByTime($schedule);
$schedule = removeSessionsFromSchedule($schedule, $remove);
}
return $schedule;
}
Related Helpers
function checkArrayEmpty($array) {
if(is_array($array) && !empty($array)) {
foreach($array as $arr) {
if(is_array($arr) && !empty($arr)) {
return true;
}
}
}
return false;
}
function subval_sort_by_time($a, $subkey) {
if (is_array($a) && count($a) > 0) {
foreach ($a as $k => $v) {
$b[$k] = strtotime($v[$subkey]);
}
asort($b);
foreach ($b as $key => $val) {
$c[] = $a[$key];
}
return $c;
}
else
return $a;
}
// Reset Index function
function resetIndexes($array) {
$new = array();
foreach($array as $date => $arr) {
//$new[$date]= array_values($arr);
$new[$date]= array_merge(array(),$arr);
}
return $new;
}
// sort by time
function sortByTime($array) {
$sorted = array();
if(is_array($array) && !empty($array)){
foreach ($array as $s => $val) {
$sorted[$s] = subval_sort_by_time($val, 'heure_debut');
}
}
return $sorted;
}
function minimiseRemoveSchedule($array) {
$new = array();
foreach($array as $date => $arr) {
$i=0;
if(is_array($arr) && !empty($arr)) {
foreach($arr as $a) {
if(isset($new[$date][$i])) {
if($new[$date][$i]['heure_fin'] == $a['heure_debut']) {
$new[$date][$i]['heure_fin'] = $a['heure_fin'];
}
else {
$i++;
$new[$date][$i]['heure_debut'] = $a['heure_debut'];
$new[$date][$i]['heure_fin'] = $a['heure_fin'];
}
} else {
$new[$date][$i]['heure_debut'] = $a['heure_debut'];
$new[$date][$i]['heure_fin'] = $a['heure_fin'];
}
}
}
}
return $new;
}
Example of Array that I pass:
$schedule = Array(
'2012-11-12' => Array(),
'2012-11-13' => Array(),
'2012-11-14' => Array( 0 => Array("employe_id" => 8 , "heure_debut" => '16:00' ,"heure_fin" => '20:00' ,"date_seance" => 2012-11-14 , "jour_id" => 3)),
'2012-11-15' => Array(
0 => Array("employe_id" => 8 , "heure_debut" => '09:00' ,"heure_fin" => '15:00' ,"date_seance" => 2012-11-15 , "jour_id" => 4),
1 => Array("employe_id" => 8 , "heure_debut" => '16:00' ,"heure_fin" => '21:00' ,"date_seance" => 2012-11-15 , "jour_id" => 4)
),
'2012-11-16' => Array(),
'2012-11-17' => Array(),
'2012-11-18' => Array(),
'2012-11-19' => Array(0 => Array("employe_id" => 8 ,"heure_debut" => '10:00' ,"heure_fin" => '22:00' ,"date_seance" => 2012-11-19 ,"jour_id" => 1)),
'2012-11-20' => Array(
0 => Array("employe_id" => 8 ,"heure_debut" => '09:00' ,"heure_fin" => '15:00' ,"date_seance" => 2012-11-20 ,"jour_id" => 2),
1 => Array("employe_id" => 8 ,"heure_debut" => '16:00' ,"heure_fin" => '20:00' ,"date_seance" => 2012-11-20 ,"jour_id" => 2)
)
);
And for the second array:
$remove = array(
'2012-11-12' => Array(),
'2012-11-13' => Array(),
'2012-11-14' => Array(),
'2012-11-15' => Array(),
'2012-11-16' => Array(),
'2012-11-17' => Array(),
'2012-11-18' => Array(),
// in this example i only have 1 absence ... I could have N absences
'2012-11-19' => Array(0 => Array("employe_id" => 8 ,"date_debut" => 2012-11-19,"date_fin" => 2012-11-19 ,"heure_debut" => '12:00:00',"heure_fin" => '14:00:00')),
'2012-11-20' => Array(),
'2012-11-21' => Array()
);
The resulting array would be:
$result = array(
Array
(
[2012-11-12] => Array()
[2012-11-13] => Array()
// no change
[2012-11-14] => Array( [0] => Array("employe_id" => 8 , "heure_debut" => 16:00 ,"heure_fin" => 20:00 ,"date_seance" => 2012-11-14 , "jour_id" => 3))
// no change
[2012-11-15] => Array(
[0] => Array("employe_id" => 8 , "heure_debut" => 09:00 ,"heure_fin" => 15:00 ,"date_seance" => 2012-11-15 , "jour_id" => 4),
[1] => Array("employe_id" => 8 , "heure_debut" => 16:00 ,"heure_fin" => 21:00 ,"date_seance" => 2012-11-15 , "jour_id" => 4)
)
[2012-11-16] => Array()
[2012-11-17] => Array()
[2012-11-18] => Array()
// since absence from 12 to 14 and we had availability from 8 to 22 instead we will have 8->12 and 14->22
[2012-11-19] => Array(
[0] => Array("employe_id" => 8 ,"heure_debut" => 08:00 ,"heure_fin" => 12:00 ,"date_seance" => 2012-11-20 ,"jour_id" => 1),
[1] => Array("employe_id" => 8 ,"heure_debut" => 14:00 ,"heure_fin" => 22:00 ,"date_seance" => 2012-11-20 ,"jour_id" => 1)
)
// no changes since no absence during those time
[2012-11-20] => Array(
[0] => Array("employe_id" => 8 ,"heure_debut" => 09:00 ,"heure_fin" => 15:00 ,"date_seance" => 2012-11-20 ,"jour_id" => 2),
[1] => Array("employe_id" => 8 ,"heure_debut" => 16:00 ,"heure_fin" => 20:00 ,"date_seance" => 2012-11-20 ,"jour_id" => 2)
)
)
I don't see why you need an exponential time recursion to execute this task. You can get away with an O(r * e^2) solution (where e is the average number of availabilities/removals per day, and r is size of removed times) via nested loop. Pseudocode below:
for removeday in remove:
define scheduleday := schedule[removeday.date]
if scheduleday not found:
continue
for removesegment in removeday:
define temparray := empty
for availsegment in scheduleday:
if availsegment.employeid != removesegment.employeid:
continue
if no overlap:
temparray.add(availsegment)
if partial overlap:
temparray.add(availsegment.split(removesegment))
scheduleday = temparray
schedule[removeday.date] := scheduleday
return schedule
The code below produces the same output for the given sample but I haven't tested all possible cases.
Working Demo
function removeSessionsFromScheduleHelper(&$schedule,&$remove) {
$change = false;
foreach($remove as $date => &$remove_ranges) {
if(empty($remove_ranges) || !isset($schedule[$date]))
continue;
foreach($remove_ranges as &$remove_range) {
foreach($schedule[$date] as $day_key => &$time) {
//start after finish, no overlap and because schedules are sorted
//next items in schedule loop will also not overlap
//break schedule loop & move to next remove iteration
if($time['heure_debut'] >= $remove_range['heure_fin'])
break;
//finish before start, no overlap
if($time['heure_fin'] <= $remove_range['heure_debut'])
continue;
//complete overlap, remove
if($time['heure_debut'] >= $remove_range['heure_debut']
&& $time['heure_fin'] <= $remove_range['heure_fin']) {
unset($schedule[$date][$day_key]);
continue;
}
//split into 2 ranges
if($time['heure_debut'] < $remove_range['heure_debut']) {
if($time['heure_fin'] > $remove_range['heure_fin']) {
$schedule[$date][] = array(
'heure_debut' => $remove_range['heure_fin'],
'heure_fin' => $time['heure_fin']
);
}
$change = true;
$time['heure_fin'] = $remove_range['heure_debut'];
continue;
}
if($time['heure_debut'] >= $remove_range['heure_debut']) {
$change = true;
$time['heure_debut'] = $remove_range['heure_fin'];
}
}
}
}
if($change) {
foreach($schedule as &$values) {
usort($values,'compare_schedule');
}
}
return $change;
}
function compare_schedule($a,$b) {
return strtotime($a['heure_debut']) - strtotime($b['heure_debut']);
}
function removeFromSchedule(&$schedule,$remove) {
foreach($remove as $k => &$v) {
foreach($v as $k2 => &$v2) {
$v2['heure_debut'] = substr($v2['heure_debut'],0,5);
$v2['heure_fin'] = substr($v2['heure_fin'],0,5);
}
}
while(removeSessionsFromScheduleHelper($schedule,$remove));
}
removeFromSchedule($schedule,$remove);
print_r($schedule);
If you don't want to add recursion to your function then you have to kind of convert it first to seconds of available schedule array matrix. Here the idea:
function scheduleToSecondsMatrix($value, $available=true){
if(!is_array($value) || empty($value))
return false;
$object = array();
foreach($value as $v) {
$s = strtotime('1970-01-01 ' . $v['heure_debut'] . (!$available ? ' +1 seconds' : '')); // ref. http://stackoverflow.com/questions/4605117/how-to-convert-a-hhmmss-string-to-seconds-with-php
$e = strtotime('1970-01-01 ' . $v['heure_fin'] . (!$available ? ' -1 seconds' : ''));
if($e < $s) continue; // logically end time should be greater than start time
while($s <= $e) {
// i use string as key as this result will be merged: http://php.net/manual/en/function.array-merge.php
$object["in_" . $s] = $available; // means in this seconds range is available
$s++;
}
}
return $object;
}
/**
* This function assume:
* - all parameters refer to only one employee
*/
function removeSessionsFromScheduleRev($schedule, $remove) {
if(!is_array($schedule) || !is_array($remove) || empty($schedule) || empty($remove)) return false;
foreach($schedule as $s => &$dispo){
if(empty($remove[$s]))
continue;
// convert the schedule to seconds array matrix, that's i call it :)
$seconds_available = scheduleToSecondsMatrix($dispo, true);
$seconds_not_available = scheduleToSecondsMatrix($remove[$s], false);
if( !$seconds_available || !$seconds_not_available ) continue; // nothing changed
$seconds_new = array_merge($seconds_available, $seconds_not_available);
$seconds_new = array_filter($seconds_new); // remove empty/false value
$new_time_schedule = array();
$last_seconds = 0;
$i=0;
foreach($seconds_new as $in_seconds => $val){
$in_seconds = intval(str_replace('in_', '', $in_seconds));
if($in_seconds > ($last_seconds+1)){
if(!empty($new_time_schedule)) $i++;
}
if(empty($new_time_schedule[$i]['start'])) $new_time_schedule[$i]['start'] = $in_seconds;
$new_time_schedule[$i]['end'] = $in_seconds;
$last_seconds = $in_seconds;
}
foreach($new_time_schedule as $idx => $val){
if($idx && empty($dispo[$idx])) $dispo[$idx] = $dispo[$idx-1];
$dispo[$idx]['heure_debut'] = date('H:i:s', $val['start']);
$dispo[$idx]['heure_fin'] = date('H:i:s', $val['end']);
}
}
return $schedule;
}
I haven't benchmark the performance yet so you may try this code on yours. I hope it works.
I think jma127 is on the right track with their pseudocode. Let me supplement their answer with some commentary.
Your basic structure is to loop through entries of $schedule, and then for each one, pull out the corresponding entry from $remove, and make some changes. As soon as a change happens, you break out of the loop, and start over again. The control structure you use to start over again is a recursive call. When you start over again, you loop again through all the entries of $schedule which you've already checked and don't need to change anymore.
Array $schedule and array $remove are related through shared subscripts. For a given index i, $remove[i] affects only $schedule[i] and no other part. If there is no entry $remove[i], then $schedule[i] is unchanged. Thus jma127 is right to restructure the loop to iterate first through entries of $remove, and have an inner code block to combine the entries of $remove[i] and $schedule[i]. No need for recursion. No need for repeatedly iterating over $schedule.
I believe this is the major reason your code becomes slow as the number of entries increases.
For a given day's entries in $remove and $schedule, the way you combine them is based on start times and end times. jma127 is right to point out that if you sort the day's entries by time (start time firstly, and end time secondly), then you can make a single pass through the two arrays, and end up with the correct result. No need for recursion or repeated looping.
I believe this is a secondary reason your code becomes slow.
Another thing I notice about your code is that you frequently put code inside a loop that isn't affected by the loop. It would be a tiny bit more efficient to put it outside the loop. For instance, your validity check for $remove and $schedule:
if (is_array($schedule) && count($schedule) > 0 \
&& is_array($remove) && count($remove) > 0)...
is repeated every time the routine is called recursively. You could instead move this check to an outer function, which calls the inner function, and the inner function won't need to check $remove and $schedule again:
function removeSessionsFromSchedule_outer($schedule, $remove) {
if ( is_array($schedule) && count($schedule) > 0
&& is_array($remove) && count($remove) > 0 ) {
$schedule = removeSessionsFromSchedule($schedule, $remove);
}
return $schedule;
}
Similarly,
foreach ($dispo as $d) {
if (isset($remove[$s])) {
$abs = $remove[$s];
} else
$abs = null;
// rest of loop....
}/*foreach*/
could be rewritten as:
if (isset($remove[$s])) {
$abs = $remove[$s];
} else
$abs = null;
foreach ($dispo as $d) {
// rest of loop....
}/*foreach*/
Another minor inefficiency is that your data structures don't contain the data in the format that you need. Instead of receiving a structure with data like:
[2012-11-14] => Array( [0] => Array(..."heure_debut" => 16:00 ...))
and each time during the loop, doing a data conversion like:
$abs_s = strtotime($a['heure_debut']);
How about having your upstream caller convert the data themselves:
["2012-11-14"] => Array([0]=>Array(..."heure_debut"=>strtotime("16:00") ...))
Another little detail is that you use syntax like 2012-11-14 and 16:00. PHP treats these as strings, but your code would be clearer if you put them in quotes to make it clear they are strings. See Why is $foo[bar] wrong? in PHP documenation Arrays.
I won't try to rewrite your code to make all these changes. I suspect you can figure that out yourself, looking at my comments and jma127's answer.
You have an availability schedule, implemented as a 2D array on day and entry number, and an absence schedule, implemented the same way, both sorted on time, and wish to update first using the second.
Both arrays are indexed the same way on their major dimension (using dates), so we can safely work on each of these rows without fear of modifying the rest of the arrays.
For a given day:
Within a day the simplest way to do it is to loop through all the $remove entries, and for each match on the employee_id, check the time and modify the schedule accordingly (something you already implemented, so we can reuse some of it). You want to keep the day schedule in order of time. The original arrays are well sorted, and if we store the modification in a new array in order creation, we won't have to sort it afterwards.
<?php
// create a schedule entry from template, with begin & end time
function schedule($tmpl, $beg, $end) {
$schedule = $tmpl;
$schedule['heure_debut'] = date("H:i", $beg);
$schedule['heure_fin'] = date("H:i", $end);
return $schedule;
}
// return one updated entry of a schedule day, based on an absence
function updateAvailability($d, $a){
// absence start/end
$dis_s = strtotime($d['heure_debut']);
$dis_e = strtotime($d['heure_fin']);
$abs_s = strtotime($a['heure_debut']);
$abs_e = strtotime($a['heure_fin']);
// Tests to see the if there is overlap between absence and availability
// (2) [a_s]---[ds - de]---[a_e]
if ($abs_s <= $dis_s && $abs_e >= $dis_e) {
return array();
}
// (7)[as == ds] && [ae < de]
else if ($abs_s == $dis_s && $abs_e < $dis_e) {
return array(schedule($d,$abs_e,$dis_e));
}
// (1)[ds] --- [as] --- [ae] --- [de] (duplicate dis with new times)
else if ($abs_s > $dis_s && $abs_e <= $dis_e) {
// new times as :
// s1 = ds-as && s2 = ae-de
return array(schedule($d,$dis_s,$abs_s), schedule($d,$abs_e,$dis_e));
}
// (3)[as] -- [ds] --- [ae] -- [de]
else if ($abs_s < $dis_s && $abs_e < $dis_e) {
return array(schedule($d,$abs_e,$dis_e));
}
// (4) [ds]---[as]--- [de]--- [ae]
else if ($abs_s > $dis_s && $abs_s < $dis_e && $abs_e > $dis_e) {
return array(schedule($d,$dis_s,$abs_s));
}
return array($d);
}
// move through all the entries of one day of schedule, and change
function updateDaySchedule($day, $absence){
$n = array();
foreach($day as $avail){
// intersect availability with absence
$a = updateAvailability($avail,$absence);
// append new entries
$n = array_merge($n, $a);
}
return $n;
}
function removeSessionsFromSchedule($schedule, $remove) {
if (!checkValidScheduleInput($schedule,$remove)
return $schedule;
foreach($remove as $day => $absences) {
// only update matching schedule day
if (isset($schedule[$day])) {
foreach ($absences as $abs)
$schedule[$day] = updateDaySchedule($schedule[$day], $abs);
}
}
return $schedule;
}
?>
There's still some room for improvement:
the $dis_s, $dis_e, etc. values in updateAvailability are recomputed each time, whereas some could be computed once, and passed in as parameter to the function. It may not be worth the hassle though.
the 'heure_debut' etc. constants could be made as defined constants:
define('HD','heure_debut');
This avoid possible typos (php will tell you if a constant is mispelled, but it won't tell you for a string literal), and make it easier for refactoring if the key names have to change.
The recursive nature of the function is your problem, nothing else in your function takes much processing power, so this should be quite fast. You really need to find a way to do this processing without recursing.