PHP: Condense array of similar strings into one merged array
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-开发者_StackOverflow10: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.
精彩评论