Class/event schedule calendar

Posted on

Problem

This is a calendar to layout an array of events/classes (from an API) in calendar format:

       Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday
time   event  |  event  |    event  |   event  | event  |   event  | event
time   event  |  event  |    event  |   event  | event  |   event  | event
time   event  |  event  |    event  |   event  | event  |   event  | event
time   event  |  event  |    event  |   event  | event  |   event  | event
time   event  |  event  |    event  |   event  | event  |   event  | event
time   event  |  event  |    event  |   event  | event  |   event  | event

This is the function the array of events is fed to:

<?php
function sortClassesByTimeThenDay($mz_classes = array()) {
    $mz_classesByTime = array();

    // Add top level value to each class to store day of week
    for($i=0;$i<count($mz_classes);$i++)
    {
        $mz_classes[$i]['day_num'] = '';
    }

    foreach($mz_classes as &$class)
    {
        /* Create a new array with a key for each time
        and corresponsing value an array of class details 
        for classes at that time. */ 
        $classTime = date_i18n("G.i", strtotime($class['StartDateTime'])); // for numerical sorting
        $display_time = (date_i18n("g:i a", strtotime($class['StartDateTime']))); 
        $classDay = date_i18n("l", strtotime($class['StartDateTime'])); // Weekday name
        $class['day_num'] = date_i18n("N", strtotime($class['StartDateTime'])); // Weekday num 1-7
        if(!empty($mz_classesByTime[$classTime])) {
            $mz_classesByTime[$classTime]['classes'] = array_merge($mz_classesByTime[$classTime]['classes'], array($class));
        } else {
            $mz_classesByTime[$classTime] = array('display_time' => $display_time, 
 'classes' => array($class));
        }
    }
    /* Timeslot keys in new array are not time-sequenced so do so*/
    ksort($mz_classesByTime);
    foreach($mz_classesByTime as $scheduleTime => &$mz_classes)
    {
        /*
        $mz_classes is an array of all classes for given time
        Take each of the class arrays and order it by days 1-7
        */
        usort($mz_classes['classes'], function($a, $b) {
            if(date_i18n("N", strtotime($a['StartDateTime'])) == date_i18n("N", strtotime($b['StartDateTime']))) {
                return 0;
            }
            return $a['StartDateTime'] < $b['StartDateTime'] ? -1 : 1;
        }); 
        $mz_classes['classes'] = week_of_timeslot($mz_classes['classes'], 'day_num');
    }
    return $mz_classesByTime;
}

function week_of_timeslot($array, $indicator){
    /*
    Make a clean array with seven corresponding slots and populate 
    based on indicator (day) for each class. There may be more than
    one event for each day and empty arrays will represent empty time slots.
    */
    $seven_days = array_combine(range(1, 7), array(array(), array(), array(),
 array(), array(), array(), array()));
        foreach($seven_days as $key => $value){
            foreach ($array as $class) {
                    if ($class[$indicator] == $key){
                        array_push($seven_days[$key], $class);
                    }
                }
            }
    return $seven_days;
    }
?>

And using HTML Table Class, the calendar is displayed:

<?php
$mz_days = sortClassesByTimeThenDay($mz_days);
$tbl = new HTML_Table('', 'mz-schedule-filter');
        $tbl->addTSection('thead');
        $tbl->addRow();
        // arguments: cell content, class, type (default is 'data' for td, pass 'header' for th)
        // can include associative array of optional additional attributes
        $tbl->addCell('', '', 'header');
        $tbl->addCell('Monday', '', 'header');
        $tbl->addCell('Tuesday', '', 'header');
        $tbl->addCell('Wednesday', '', 'header');
        $tbl->addCell('Thursday', '', 'header');
        $tbl->addCell('Friday', '', 'header');
        $tbl->addCell('Saturday', '', 'header');
        $tbl->addCell('Sunday', '', 'header');

$tbl->addTSection('tbody');
foreach($mz_days as $classDate => $mz_classes)
    {                   
        $tbl->addRow();
        $tbl->addCell($time_of_day, 'hidden', 'data');
        $tbl->addCell($mz_classes['display_time']);
        foreach($mz_classes['classes'] as $key => $classes)
        {
            if(empty($classes)){
                $class_details = '';
                }else{
                $class_details = '';
                $class_separator = (count($classes) > 1) ? '<hr/>' : '';
                foreach($classes as $class){
                        $className = $class['ClassDescription']['Name'];
                        }
                    }
                }
            $tbl->addCell($class_details, 'mz_description_holder');
        }//end foreach mz_classes
    }//end foreach mz_days
$return .= $tbl->display();
?>

I’d like input on styling, comments, syntax, and I’m especially interested in ways to reduce the number of loops being run in sortClassesByTimeThenDay().

It seems like maybe array_map could be used to insert the ordered events into the seven day matrix/array or possibly usort and week_of_timeslot could be combined, but not sure how. At the moment I’m basking in the miracle that I made it work at all.

Solution

Prefer foreach when possible

In this loop:

for($i=0;$i<count($mz_classes);$i++)
{
    $mz_classes[$i]['day_num'] = '';
}

The loop index is tedious, and distracting.
At a higher level,
your actual purpose is to initialize 'day_num' in each element,
the array index is only there for technical reasons,
it’s not really part of your logic.

A foreach loop without an index variable can help focus on your actual purpose:

foreach ($mz_classes as &$mz_class) {
    $mz_class['day_num'] = '';
}

The result is cleaner and shorter too, so overall better in many ways.

Avoid unnecessary operations + limit scope

In this code:

    $display_time = (date_i18n("g:i a", strtotime($class['StartDateTime']))); 
    $classDay = date_i18n("l", strtotime($class['StartDateTime'])); // Weekday name
    $class['day_num'] = date_i18n("N", strtotime($class['StartDateTime'])); // Weekday num 1-7
    if(!empty($mz_classesByTime[$classTime])) {
        $mz_classesByTime[$classTime]['classes'] = array_merge($mz_classesByTime[$classTime]['classes'], array($class));
    } else {
        $mz_classesByTime[$classTime] = array('display_time' => $display_time, 
 'classes' => array($class));
    }

$classDay is never used, so it shouldn’t be there.

$display_time is only used in the else branch.
So it should be created there, for many reasons:

  • Avoid an unnecessary operation when the else branch is not reached
  • Declare variables close to where they are actually used
  • Limit variables to the smallest possible scope: a weak reason in PHP, but a good habit nonetheless

Formatting

This writing style is too compact:

for($i=0;$i<count($mz_classes);$i++)

It would be more readable to follow good spacing conventions in other languages, like this:

for ($i = 0; $i < count($mz_classes); $i++)

Leave a Reply

Your email address will not be published. Required fields are marked *