PHP: Condense array of similar strings into one merged array - php

Working with an array of dates (opening times for a business). I want to condense them to their briefest possible form.
So far, I started out with this structure
Array
(
[Mon] => 12noon-2:45pm, 5:30pm-10:30pm
[Tue] => 12noon-2:45pm, 5:30pm-10:30pm
[Wed] => 12noon-2:45pm, 5:30pm-10:30pm
[Thu] => 12noon-2:45pm, 5:30pm-10:30pm
[Fri] => 12noon-2:45pm, 5:30pm-10:30pm
[Sat] => 12noon-11pm
[Sun] => 12noon-9:30pm
)
What I want to achieve is this:
Array
(
[Mon-Fri] => 12noon-2:45pm, 5:30pm-10:30pm
[Sat] => 12noon-11pm
[Sun] => 12noon-9:30pm
)
I've tried writing a recursive function and have managed to output this so far:
Array
(
[Mon-Fri] => 12noon-2:45pm, 5:30pm-10:30pm
[Tue-Fri] => 12noon-2:45pm, 5:30pm-10:30pm
[Wed-Fri] => 12noon-2:45pm, 5:30pm-10:30pm
[Thu-Fri] => 12noon-2:45pm, 5:30pm-10:30pm
[Sat] => 12noon-11pm
[Sun] => 12noon-9:30pm
)
Can anybody see a simple way of comparing the values and combining the keys where they're similar? My recursive function is basically two nested foreach() loops - not very elegant.
Thanks,
Matt
EDIT: Here's my code so far, which produces the 3rd array above (from the first one as input):
$last_time = array('t' => '', 'd' => ''); // blank array for looping
$i = 0;
foreach($final_times as $day=>$time) {
if($last_time['t'] != $time ) { // it's a new time
if($i != 0) { $print_times[] = $day . ' ' . $time; }
// only print if it's not the first, otherwise we get two mondays
} else { // this day has the same time as last time
$end_day = $day;
foreach($final_times as $day2=>$time2) {
if($time == $time2) {
$end_day = $day2;
}
}
$print_times[] = $last_time['d'] . '-' . $end_day . ' ' . $time;
}
$last_time = array('t' => $time, 'd' => $day);
$i++;
}

I don't think there is a particularly elegant solution to this. After much experimenting with the built in array_* functions trying to find a nice simple solution, I gave up and came up with this:
$lastStart = $last = $lastDay = null;
$new = array();
foreach ($arr as $day => $times) {
if ($times != $last) {
if ($last != null) {
$key = $lastStart == $lastDay ? $lastDay : $lastStart . '-' . $lastDay;
$new[$key] = $last;
}
$lastStart = $day;
$last = $times;
}
$lastDay = $day;
}
$key = $lastStart == $lastDay ? $lastDay : $lastStart . '-' . $lastDay;
$new[$key] = $last;
It only uses one foreach loop as opposed to your two, as it keeps a bunch of state. It'll only merge adjacent days together (i.e., you won't get something like Mon-Tue,Thu-Fri if Wednesday is changed, you'll get two separate entries).

I'd approach it by modelling it as a relational database:
day start end
1 12:00 14:45
1 17:30 22:30
...
Then its fairly easy to reduce - there are specific time intervals:
SELECT DISTINCT start, end
FROM timetable;
And these will occur on specific days:
SELECT start, end, GROUP_CONCAT(day) ORDER BY day SEPERATOR ','
FROM timetable
GROUP BY start,end
(this uses the MySQL-only 'group_concat' function - but the method is the same where this is not available)
would give:
12:00 14:45 1,2,3,4,5
17:30 22:30 1,2,3,4,5
12:00 23:00 6
12:00 21:30 7
Then it's fairly simple to work out consecutive date ranges from the list of days.
C.

As an alternative, I managed to cobble together a version using array_* functions. At some point though, 'elegance', 'efficiency' and 'readability' all packed up and left. It does, however, handle the edge cases I mentioned in the other answer, and it left me with a nice warm glow for proving it could be done in a functional manner (yet at the same time a sense of shame...)
$days = array_keys($arr);
$dayIndices = array_flip($days);
var_dump(array_flip(array_map(
function ($mydays) use($days, $dayIndices) {
return array_reduce($mydays,
function($l, $r) use($days, $dayIndices) {
if ($l == '') { return $r; }
if (substr($l, -3) == $days[$dayIndices[$r] - 1]) {
return ((strlen($l) > 3 && substr($l, -4, 1) == '-') ? substr($l, 0, -3) : $l) . '-' . $r;
}
return $l . ',' . $r;
}, '');
}, array_map(
function ($day) use ($arr) {
return array_keys($arr, $arr[$day]);
}, array_flip($arr)
)
)));
I tested it with this input:
'Mon' => '12noon-2:45pm, 5:30pm-10:30pm',
'Tue' => '12noon-2:45pm, 5:30pm-10:30pm',
'Wed' => '12noon-2:45pm, 5:30pm-10:00pm',
'Thu' => '12noon-2:45pm, 5:30pm-10:30pm',
'Fri' => '12noon-2:45pm, 5:30pm-10:00pm',
'Sat' => '12noon-2:45pm, 5:30pm-10:30pm',
'Sun' => '12noon-9:30pm'
And got this:
["Mon-Tue,Thu,Sat"]=> string(29) "12noon-2:45pm, 5:30pm-10:30pm"
["Wed,Fri"]=> string(29) "12noon-2:45pm, 5:30pm-10:00pm"
["Sun"]=> string(13) "12noon-9:30pm"
Basically, the array_map at the end transforms the input into an associative array of times to an array of days that they occur on. The large block of code before that reduces those days into a nicely formatted string using array_reduce, consulting the $days and $dayIndices arrays to check if days are consecutive or not.

Related

Grouping with hyphenate not working with i18N for weekdays

I am working on a snippet for displaying opening hours and it works fine in english language and when I change the keys of array to another language it doesn't hyphenate the letters instead it does separation by comma.
What am I doing Wrong?
Below is the PHP code with 2 arrays with 1 commented which is in english and which works fine. Another is an italian langugage weekdays
<?php
/*
// english weekdays
$openHours = array(
'Mon' => '9am-7pm',
'Tue' => '9am-7pm',
'Wed' => '9am-7pm',
'Thu' => '9am-10pm',
'Fri' => 'closed',
'Sat' => '9am-10pm',
'Sun' => '9am-10pm'
);
*/
// italian weekdays
$openHours = array(
'lunedì' => '9am-7pm',
'martedì' => '9am-7pm',
'mercoledì' => '9am-7pm',
'giovedì' => '9am-10pm',
'venerdì' => 'closed',
'sabato' => '9am-10pm',
'domenica' => '9am-10pm'
);
$new_array = array();
foreach($openHours as $key => $value)
{
if(in_array($value,$new_array))
{
$key_new = array_search($value, $new_array);//to get the key of element
unset($new_array[$key_new]); //remove the element
$key_new = $key_new.','.$key; //updating the key
$new_array[$key_new] = $value; //inserting new element to the key
}
else
{
$new_array[$key] = $value;
}
}
foreach ($new_array as $days=>$time){
$daylist = explode(',',$days);
if ($time!='closed'){
if (count($daylist)>2){
$limit = count($daylist)-1;
$first = $daylist[0];
$last = $daylist[$limit];
//loop will go here.
if (date('D', strtotime('+'.$limit.' days', strtotime($first)))==$last){
echo $first.'-'.$last.' '.$time.'<br>';
} else {
$sep = '';
foreach ($daylist as $sepdays){
echo $sep.$sepdays;
$sep = ',';
}
echo ' '.$time.'<br>';
}
} else {
echo $days.' '.$time.'<br>';
}
} else {
$daylist = explode(',',$days);
foreach ($daylist as $sepdays){
echo $sepdays.' '.$time.'<br>';
}
}
}
?>
RESULT
Current Result what am getting with italian language.
lunedì,martedì,mercoledì 9am-7pm
venerdì closed
giovedì,sabato,domenica 9am-10pm
Expected RESULT
This is what I'm expecting.
lunedì-mercoledì 9am-7pm
venerdì closed
giovedì,sabato,domenica 9am-10pm
You are using your array's keys within date and strtotime functions to do your comparisons, both functions works for English. If you need to do it on other languages you should use setlocale and strftime, it will be a lot more complicated process. My suggestions:
Use numeric representation of the days of the week (0-6) and on display, replace the number with the value for the desired language.
Use multidimensional arrays including the numeric day of the week and the opening hours.

WooCommerce Bookings - Comparing two timestamps with PHP on custom dates format

Using WooCommerce Bookings plugin, I'm developing a system where I have to compare two dates with PHP.
I have the next sql query to get this date:
$consulta2="SELECT meta_value FROM pwkynbjbwoocommerce_order_itemmeta WHERE meta_key='Fecha de Reserva' AND order_item_id=".$row["order_item_id"];
Fecha de Reserva give us a spanish date like septiembre 2016, or septiembre 1, 2016 (September 2016, or September 1,2016) for example.
I want to compare one date with "today", so I have tried to use this PHP code:
if (strtotime($row2["meta_value"])>time()){}
But it doesn't work.
How can I achieve this?
Thanks
Yes is it possible with this custom function where I list in an associative array the spanish month with numerical month as key, and I reorder the date to output it through strtotime() function. This function can also return the current time with 'now' as parameter.
This is the function code:
function bk_date( $date ) {
// Removing the coma (if there is a coma + a space)
$my_time = str_replace ( ',', '', $date );
// or replacing the coma by a space (if there is a coma alone without a space)
// $my_time = str_replace ( ',', ' ', $the_date );
$my_time_arr = explode( ' ', $my_time );
$my_time_count = count( $my_time_arr );
$month = $my_time_arr[0];
if ( count( $my_time_arr ) > 2 ) { // When there is the month, the day and the year
// Formating the day in 2 digits
$day = $my_time_arr[1] < 10 ? '0'.$my_time_arr[1] : $my_time_arr[1];
$year = $my_time_arr[2];
} else { // When there is the month, the day and the year
$year = $my_time_arr[1];
}
// Array of spanish month
$month_arr = array('01' => 'enero', '02' => 'febrero', '03' => 'marzo', '04' => 'abril', '05' => 'mayo', '06' => 'junio', '07' => 'julio', '08' => 'agosto', '09' => 'septiembre', '10' => 'octubre', '11' => 'noviembre', '12' => 'diciembre');
// Browse the list of month and compare (when $value match, it's replace by the index $key
foreach ( $month_arr as $key => $value ) {
if ( $month == $value ) {
$month = $key;
break;
}
}
if ( count( $my_time_arr ) > 2 )
$result = $year . '-' . $month . '-' . $day;
else
$result = $year . '-' . $month;
return $date == 'now' ? strtotime( $date ) : strtotime( $result );
}
This code goes on function.php file of your active child theme or theme.
As I don't really know if the date is like septiembre 1, 2016 or like septiembre 1,2016 (without a space after the coma), you will find a commented alternative in the code above.
My code work also for febrero, 2016 or febrero 2016 date format.
Please check that I haven't make any mistakes in the month names located in $month_arr array…
Usage:
echo bk_date($row2["meta_value"]); // display the timestamp of your spanish date.
// Comparing 2 timestamps (bk_date('now') return the "current time").
if ( bk_date($row2["meta_value"]) > bk_date('now') ) {
// do something
}

Performance with time related algorithm

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.

Get value of previous array key in PHP

I am trying to do a Drug Half life calculator with PHP. I want to pass in the amount of the drug taken per day in MG's and pass in the Half-life hours, then it will calculate how much of the drug is left after X amount of time and how much is still left from previous doses.
So far this is what I have...
function calcHalfLife( $mgTaken , $drugHalfLifeHours , $day = 1 ) {
//total number of half-lifes elapsed
$total_half_lifes = ($day * 24) / $drugHalfLifeHours;
//total reduction in dosage
$reductionFactor = pow( 0.5 , $total_half_lifes );
//return the current dosage in the person's system
return round( $mgTaken * $reductionFactor , 8 );
}
Then I am working on this function below which will let me pass in an Array of Days and the MG taken for each day, the function should then iterate the array and run the function above on each day's value.
function HalfLifeChart(array $days, $drugHalfLifeHours ) {
$out = array();
foreach ($days as $day => $dosage) {
$out[$day] = calcHalfLife( $dosage , $drugHalfLifeHours , 1 );
}
return $out;
}
Example usage...
$day = array(1 => 30,
2 => 0,
3 => 0,
4 => 40,
5 => 30,
6 => 10,
7 => 60);
echo '<br><pre>';
print_r(HalfLifeChart( $day, 4.5));
echo '</pre><br><br>';
Now I have a pretty good start but the HalfLifeChart function is where I need to do more work, right now it will run the Half-life calculations on the number passed for each day which is good, but I need to get the result from the previous day and add that to the MG taken on the current day and then run the Calculations on that number.
So for example, if I have 0.8043mg left from the previous day and I took 30mg today, then the calculation should be ran on 0.8043 + 30 and then pass that result through my Half life calculator function.
I am not sure how to grab the result from the previous day though, any help please?
Why don't you store the result of the previous day on another variable?
Something like:
function HalfLifeChart(array $days, $drugHalfLifeHours ) {
$out = array();
$prevDay = 0;
foreach ($days as $k => $v) {
$out[$k] = calcHalfLife( $v , $drugHalfLifeHours , 1 ); //change this
$prevDay = $out[$k];
}
return $out;
}
function HalfLifeChart(array $days, $drugHalfLifeHours ) {
$out=array();
$remains=0;
foreach ($days as $day => $dosage) {
$total=$remains+$dosage;
$out[$day]=$total;
$remains=calcHalfLife( $total , $drugHalfLifeHours , 1 );
}
return $out;
}
gives you
print_r(HalfLifeChart( $day, 4.5));
Array
(
[1] => 30
[2] => 0.74409424
[3] => 0.01845587
[4] => 40.00045776
[5] => 30.99213701
[6] => 10.76870236
[7] => 60.26709765
)
Just store it.
function HalfLifeChart(array $days, $drugHalfLifeHours ) {
$out = array();
$yesterday = 0;
foreach ($days as $k => $v) {
$out[$k] = calcHalfLife($v + $yesterday, $drugHalfLifeHours, 1);
$yesterday = $out[$k];
}
return $out;
}

Combine days where opening hours are similar

How might I code a function in PHP (with CodeIgniter) to merge days with similar opening hours of a store together. For example, if we have:
Mon 9am-5pm
Tue 9am-5pm
Wed 9am-5pm
Thu 9am-5pm
Fri 9am-5pm
Sat 9am-7pm
Sun 9am-7pm
I want the code to simplify it to:
Mon-Fri 9am-5pm
Sat-Sun 9am-7pm
How do I do this without a long list of if/else or case ifs? I'm using CodeIgniter..
<?php
$openHours = array(
'Mon' => '9am-5pm',
'Tue' => '9am-5pm',
'Wed' => '9am-9pm',
'Thu' => '9am-5pm',
'Fri' => '9am-5pm',
'Sat' => '9am-7pm',
'Sun' => '9am-7pm'
);
$summaries = array();
foreach ($openHours as $day => $hours) {
if (count($summaries) === 0) {
$current = false;
} else {
$current = &$summaries[count($summaries) - 1];
}
if ($current === false || $current['hours'] !== $hours) {
$summaries[] = array('hours' => $hours, 'days' => array($day));
} else {
$current['days'][] = $day;
}
}
foreach ($summaries as $summary) {
if (count($summary['days']) === 1) {
echo reset($summary['days']) . ' ' . $summary['hours'] . PHP_EOL;
} else {
echo reset($summary['days']) . '-' . end($summary['days']) . ' ' . $summary['hours'] . PHP_EOL;
}
}
codepad sample
If you want it in the following format:
Sun-Tue, Fri-Sat: 11am-12am; Wed: 10am-12am; Thu: 9am-12am
Which will group days and handle different times within the week:
<?php
$open_hours = array (
"Sun" => "11am-12am",
"Mon" => "11am-12am",
"Tue" => "11am-12am",
"Wed" => "10am-12am",
"Thu" => "9am-12am",
"Fri" => "11am-12am",
"Sat" => "11am-12am"
);
$result = [];
foreach ($open_hours as $day => $hours) {
if (empty($result) || $previous !== $hours) {
$result[$hours][] = $day;
} elseif ($previous === $hours) {
$key = array_key_last($result[$hours]);
$current = strtok($result[$hours][$key], '-');
$result[$hours][$key] = $current.'-'.$day;
}
$previous = $hours;
}
// build output (joining days with ,)
$output = [];
foreach ($result as $hours => $days) {
$output[] = implode(', ', $days).': '.$hours;
}
// join with ;'s and output
echo implode('; ', $output);
https://3v4l.org/tKOlI

Categories