5
\$\begingroup\$

I have a list of volunteer shifts. The shifts are time ranges- for example, 9AM-2PM and 12PM-5PM. I need to check by how many minutes multiple selected shifts overlap, and then throw an error if the overlap is too high.

I coded this for my website and it works great. Just purely out of intellectual curiosity, is there a faster and/or more organized way to code this?

Below is the code. I've modified it so that you can just copy/paste it into a PHP file and run it yourself. Sample data is included.

I am open to all feedback. In particular, I am looking for ways to code the main loop more elegantly. There's a pretty hard-to-read conditional in there right now. The purpose of the conditional is to prevent a pair of shifts from getting checked twice. That is, shift ID's 49/50 and 50/49 are going to have the same overlap, so they shouldn't be checked twice.

<?php

    define('MAX_MINUTES_OF_SHIFT_OVERLAP_FOR_PUBLIC', 65);

    function check_for_overlapping_shifts($list_of_shifts, $shift_ids_to_check)
    {
        if ( count($shift_ids_to_check) >= 2 )
        {
            $overlapping_shifts = array();
            foreach ( $shift_ids_to_check as $outer_shift_id )
            {
                foreach ( $shift_ids_to_check as $inner_shift_id )
                {
                    if (
                        $outer_shift_id != $inner_shift_id &&
                        ! (
                            sql_search_result_array_by_key1_value1($overlapping_shifts, 'outer_shift_id', $inner_shift_id) &&
                            sql_search_result_array_by_key1_value1($overlapping_shifts, 'inner_shift_id', $outer_shift_id)
                        )
                    )
                    {
                        $outer_shift_start_time = strtotime($list_of_shifts[$outer_shift_id]['shift_start_time']);

                        $outer_shift_end_time = strtotime($list_of_shifts[$outer_shift_id]['shift_end_time']);

                        $inner_shift_start_time = strtotime($list_of_shifts[$inner_shift_id]['shift_start_time']);

                        $inner_shift_end_time = strtotime($list_of_shifts[$inner_shift_id]['shift_end_time']);

                        // https://stackoverflow.com/questions/325933/determine-whether-two-date-ranges-overlap
                        if ( $outer_shift_start_time <= $inner_shift_end_time && $outer_shift_end_time >= $inner_shift_start_time )
                        {
                            if ( $outer_shift_end_time > $inner_shift_start_time )
                            {
                                $minutes_of_overlap = ($outer_shift_end_time - $inner_shift_start_time) / 60;
                            }
                            else
                            {
                                $minutes_of_overlap = ($inner_shift_end_time - $outer_shift_start_time) / 60;
                            }

                            array_push($overlapping_shifts, array(
                                'outer_shift_id' => $outer_shift_id,
                                'inner_shift_id' => $inner_shift_id,
                                'outer_shift_name' => $list_of_shifts[$outer_shift_id]['shift_name'],
                                'inner_shift_name' => $list_of_shifts[$inner_shift_id]['shift_name'],
                                'minutes_of_overlap' => $minutes_of_overlap
                            ));
                        }
                    }
                }
            }

            var_export($overlapping_shifts);
            echo "<br /><br />";

            foreach ( $overlapping_shifts as $key => $shift )
            {
                if ( $shift['minutes_of_overlap'] > MAX_MINUTES_OF_SHIFT_OVERLAP_FOR_PUBLIC )
                {
                    echo ($overlapping_shifts[$key]['outer_shift_name'] . " and " . $overlapping_shifts[$key]['inner_shift_name'] . " overlap by more than " . MAX_MINUTES_OF_SHIFT_OVERLAP_FOR_PUBLIC . " minutes (" . $overlapping_shifts[$key]['minutes_of_overlap'] . " minutes to be exact)<br />");
                }
            }
        }
    }

    /* Input must be an array with 2 layers. First layer is just array keys in numerical order. Second layer is field => field value. This is a very common array format for SQL results. Example:
        [0] =>
            'group_id' => 5
            'group_name' => 'Test Group 2'
        [1] =>
            'group_id' => 2
            'group_name' => 'Test Group 1'
    */
    // use === and !== in booleans to avoid having a '0' value act like a NULL value
    function sql_search_result_array_by_key1_value1($array, $search_key, $search_value)
    {
        foreach ( $array as $key => $level2 )
        {
            if ( $level2[$search_key] == $search_value )
            {
                return TRUE;
            }
        }

        return NULL;
    }

    // Example data
    $list_of_shifts = array(
        49 => array(
            'shift_name' => 'A1 Fri Packet Pickup AM',
            'shift_start_time' => '2017-12-08 11:00:00',
            'shift_end_time' => '2017-12-08 15:30:00'
        ),
        50 => array(
            'shift_name' => 'A2 Fri Packet Pickup PM',
            'shift_start_time' => '2017-12-08 15:00:00',
            'shift_end_time' => '2017-12-08 18:30:00'
        ),
        51 => array(
            'shift_name' => 'B1 Sat Packet Pickup',
            'shift_start_time' => '2017-12-09 05:30:00',
            'shift_end_time' => '2017-12-09 09:30:00'
        ),
        52 => array(
            'shift_name' => 'B2 Sat Water Station 1',
            'shift_start_time' => '2017-12-09 06:00:00',
            'shift_end_time' => '2017-12-09 11:00:00'
        ),
        53 => array(
            'shift_name' => 'B3 Sat Water Station 2',
            'shift_start_time' => '2017-12-09 06:00:00',
            'shift_end_time' => '2017-12-09 11:00:00'
        ),
        54 => array(
            'shift_name' => 'C1 Sat Finish Line',
            'shift_start_time' => '2017-12-09 06:30:00',
            'shift_end_time' => '2017-12-09 11:00:00'
        )
    );
    $shift_ids_that_user_submitted = array(49, 50, 51, 52, 53, 54);

    check_for_overlapping_shifts($list_of_shifts, $shift_ids_that_user_submitted);

Output is:

array ( 0 => array ( 'outer_shift_id' => 49, 'inner_shift_id' => 50, 'outer_shift_name' => 'A1 Fri Packet Pickup AM', 'inner_shift_name' => 'A2 Fri Packet Pickup PM', 'minutes_of_overlap' => 30, ), 1 => array ( 'outer_shift_id' => 51, 'inner_shift_id' => 52, 'outer_shift_name' => 'B1 Sat Packet Pickup', 'inner_shift_name' => 'B2 Sat Water Station 1', 'minutes_of_overlap' => 210, ), 2 => array ( 'outer_shift_id' => 51, 'inner_shift_id' => 53, 'outer_shift_name' => 'B1 Sat Packet Pickup', 'inner_shift_name' => 'B3 Sat Water Station 2', 'minutes_of_overlap' => 210, ), 3 => array ( 'outer_shift_id' => 51, 'inner_shift_id' => 54, 'outer_shift_name' => 'B1 Sat Packet Pickup', 'inner_shift_name' => 'C1 Sat Finish Line', 'minutes_of_overlap' => 180, ), 4 => array ( 'outer_shift_id' => 52, 'inner_shift_id' => 53, 'outer_shift_name' => 'B2 Sat Water Station 1', 'inner_shift_name' => 'B3 Sat Water Station 2', 'minutes_of_overlap' => 300, ), 5 => array ( 'outer_shift_id' => 52, 'inner_shift_id' => 54, 'outer_shift_name' => 'B2 Sat Water Station 1', 'inner_shift_name' => 'C1 Sat Finish Line', 'minutes_of_overlap' => 270, ), 6 => array ( 'outer_shift_id' => 53, 'inner_shift_id' => 54, 'outer_shift_name' => 'B3 Sat Water Station 2', 'inner_shift_name' => 'C1 Sat Finish Line', 'minutes_of_overlap' => 270, ), )

B1 Sat Packet Pickup and B2 Sat Water Station 1 overlap by more than 65 minutes (210 minutes to be exact)
B1 Sat Packet Pickup and B3 Sat Water Station 2 overlap by more than 65 minutes (210 minutes to be exact)
B1 Sat Packet Pickup and C1 Sat Finish Line overlap by more than 65 minutes (180 minutes to be exact)
B2 Sat Water Station 1 and B3 Sat Water Station 2 overlap by more than 65 minutes (300 minutes to be exact)
B2 Sat Water Station 1 and C1 Sat Finish Line overlap by more than 65 minutes (270 minutes to be exact)
B3 Sat Water Station 2 and C1 Sat Finish Line overlap by more than 65 minutes (270 minutes to be exact)
\$\endgroup\$
1
\$\begingroup\$

Feedback

While it looks like a lot of logic, it looks sufficient for the task. Good work in using strtotime() to convert the dates.

Suggestion to improve performance, and eliminate that lookup function

catalogue

Consider the scenario of putting cards in a box. If you later want to know if a card is already in the box before deciding to make another one, do you want to search the whole box? Wouldn’t it be easier if you could check if a card exists by going to an exact spot? The same is true for the storage technique of the sub-arrays in $overlapping_shifts.

Instead of just pushing the sub-arrays into the array of overlapping arrays, set the sub-array at a specific index- making it an associative array instead of an indexed array. That way the time required for look-ups is minimal (I.e. \$O(1)\$ complexity) instead of having to search through the array (I.e. \$O(n)\$ complexity).

So when setting the key, you could concatenate the outer and inner shift IDs:

$overlapping_shifts[$outer_shift_id.'_'.$inner_shift_id] = array(
    'outer_shift_name' => $list_of_shifts[$outer_shift_id]['shift_name'],
    'inner_shift_name' => $list_of_shifts[$inner_shift_id]['shift_name'],
    'minutes_of_overlap' => $minutes_of_overlap
);

Notice that the shift IDs are no longer present inside the sub-array. After seeing the next section, it should be clear that those aren’t needed anymore.

That way, the check to see if the shift IDs exist in the array of overlapping shifts becomes simpler by utilizing array_key_exists():

if (
    $outer_shift_id != $inner_shift_id && 
    !array_key_exists($inner_shift_id.'_'.$outer_shift_id, $overlapping_shifts)
)

And the function sql_search_result_array_by_key1_value1() can be eliminated.

See a demonstration in this Playground example.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.