1

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++;

}
4
  • Your problem is ill-defined. Is the program supposed to know that Tue comes after Mon? Is "Mon-Tue, Fri" a valid set? What exactly are the rules for the folding? You should start there, and perhaps then an implementation will become more evident. Commented Jun 8, 2010 at 11:39
  • Can you show us the function that you're using so far? Commented Jun 8, 2010 at 11:42
  • Hmm, good points. It doesn't have to be intelligent, it should just discover the range, eg: if Mon, Tue, Wed, Thu and Fri all have the same value, then it should concatenate the first and last keys (Mon and Fri) together. Although you're right, "Mon-Tue, Fri" should be valid too. I'll have another think through it... Commented Jun 8, 2010 at 11:43
  • Cetra: edited to include my (messy) current function. Commented Jun 8, 2010 at 11:48

3 Answers 3

1

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).

Sign up to request clarification or add additional context in comments.

1 Comment

You're a hero, thank you! I'll have a skim through this and see if I can find a way to modify it to cater for those edge cases like you mention. Thank you for helping :)
1

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.

1 Comment

Thanks - these dates are already coming from a relational database, and I had to work with them to turn them into the form I posted above (basically tweak the dates and group together multiple times for the same day). I might have a look at doing more on the SQL side but for now I think Chris Smith's solution above is the way to go.
1

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.

2 Comments

Well, now I'm glad I didn't get around to attempting to do this myself - I'd never have the skill to figure out all of those anonymous functions and array_* calls. Thank you! One issue: the code gives me this error: Parse error: syntax error, unexpected T_FUNCTION, expecting ')' on the line starting: function ($mydays) use ($days, $dayIndices) { Any ideas?
@Matt Anonymous functions like the ones splattered throughout that code were introduced in PHP 5.3.0. I guess you're using an earlier version. If you really want to do it this way, you'll need to pull the anonymous functions out into proper functions (and use globals instead of closures). I think it'd be easier to modify the other solution though ;)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.