-->

Staff schedule working alone minutes

2020-05-09 22:21发布

问题:

I have list of times for staff. I need to find out if any of the staff was working alone and how many minutes they were working alone for the day

| staff| start | end   |
|:---  |:---   |:---   |
| 1    | 11:05 | 20:00 | 
| 2    | 11:00 | 17:00 |
| 3    | 19:00 | 03:00 |
| 4    | 13:00 | 20:00 |
| 5    | 19:00 | 03:00 |

With Andreas' help, following is the code that gets the first and last person who was working alone with alone minutes, but its not quite right. Because if there were 3 people with different times that worked alone, it will give a problem. https://3v4l.org/6OmjO

$staff = array(1,2,3,4,5);
$start = array("11:05", "11:00", "19:00", "13:00", "19:00");
$end = array("20:00", "17:00", "03:00", "20:00", "03:05");

array_multisort($start, $end, $staff);

$aloneStart = (strtotime($start[1]) - strtotime($start[0])) / 60; // first and second items are the ones that may be working alone at start
$aloneEnd = (strtotime($end[count($end) - 1]) - strtotime($end[count($end) - 2])) / 60; // last and second to last are the ones that may be working alone at end

if ($aloneStart > 0)
{
    $staffAloneStart = $staff[0]; //must be the first who worked alone
    echo "minutes alone at start: " . $aloneStart . " and it was " . $staffAloneStart . "\n";
}

if ($aloneEnd > 0)
{
    $staffAloneEnd = $staff[count($end) - 1]; // must be the last to end that worked alone
    echo "minutes alone at end: " . $aloneEnd . " and it was " . $staffAloneEnd . "\n";
}

$aloneTime = intval($aloneStart) + intval($aloneEnd);
echo "total time alone " . $aloneTime;

with following array, you will see the minutes for first user needs to be more then 5 minutes, because he is working alone more at evening.

$staff = array(1, 2, 3, 4, 5);
$start = array("11:05", "11:10", "19:00", "13:00", "19:00");
$end = array("20:00", "17:00", "03:00", "16:00", "03:00");

回答1:

I am doing a complete re-write of my answer so that it is clear and flows in the proper order. I made a couple of minor refinements from my previous method, but nothing drastic.

First is the data preparation code. I convert the OP's hh:mm time in and out values to simple minute values, while maintaining employee ids as keys.

// My test data in OP's format to start with:
$staff=[1,2,3];
$start=['11:00','13:00','17:00'];
$end=['21:00','15:00','19:00'];

// My data preparation method:
foreach($staff as $i=>$v){
    $on=explode(':',$start[$i]);  // separate hh from mm of start of shift
    $on_minutes=$on[0]*60+$on[1];  // calculate total minutes from start of day
    $off=explode(':',$end[$i]);   // separate hh from mm of end of shift
    $off_minutes=($off[0]+($on[0]>$off[0]?24:0))*60+$off[1];  // calculate minutes from start of day, factoring shift that run past midnight
    $shifts[$v]=[$on_minutes,$off_minutes];  // store prepared data for future processes
}
/*
  (new prepared array):
  $shifts=[
    1=>[660,1260],
    2=>[780,900],
    3=>[1020,1140]
  ];
*/

This is the data processing snippet. I have built in a shortcut -- if one employee shares an identical shift with another employee, then the first employee is immediately deemed to have zero minutes alone (obviously). Otherwise, an employee's shift is compared one by one against the other employees' shifts to determine how many minutes they are alone.

function whittle($colleague_shifts,$pieces_of_shift){  // initially, PoS is only one element
    foreach($colleague_shifts as $k=>$cs){
        foreach($pieces_of_shift as $i=>$ps){
            if($cs[0]<=$ps[0] && $cs[1]>=$ps[1]){
                unset($pieces_of_shift[$i]);
                continue;  // fully covered by coworker
            }
            $temp=[];
            if($ps[0]<$cs[0] && $cs[0]<$ps[1]){
                $temp[]=[$ps[0],$cs[0]];    // push new unmatched start into temp PoS array
            }
            if($ps[1]>$cs[1] && $cs[1]>$ps[0]){
                $temp[]=[$cs[1],$ps[1]];    // push new unmatched end into temp PoS array
            }
            if($temp){
                array_splice($pieces_of_shift,$i,1,$temp);  // replace the current PoS with 1 or 2 new PoS subarrays
            }
        }
        if(!$pieces_of_shift){
            return 0;  // no minutes alone
        }
    }
    // subtract all end alone minutes from all start alone minutes
    return array_sum(array_column($pieces_of_shift,1))-array_sum(array_column($pieces_of_shift,0));
}

foreach($shifts as $id=>$s){
    $colleague_shifts=array_diff_key($shifts,[$id=>'']);  // generate array excluding target worker's shift
    if(in_array($s,$colleague_shifts)){  // check for same start and end times elsewhere
        $alone[$id]=0;  // exact duplicate allows shortcut as "never alone"
    }else{
        $alone[$id]=whittle($colleague_shifts,[$s]);  // whittle down times where target employee is alone
    }
}
var_export($alone);

Output:

array (
  1 => 360,  // alone from 11am-1pm, 3pm-5pm, and 7pm-9pm
  2 => 0,   // never alone
  3 => 0,   // never alone
)

To help you follow what is happening inside of whittle()

  • Staff #1 starts with a full shift from 660 to 1260. ($pieces_of_shift is an array with just one subarray with two elements - start minutes and end minutes)
    $pieces_of_shift=[[660,1260]];
  • After being compared against Staff #2, the original $pieces_of_shift subarray is replaced by two new subarrays -- the alone time at the start of the shift, and the alone time at the end of the shift: 660 to 780 and 900 to 1260.
    $pieces_of_shift=[[660,780],[900,1260]];
  • Then Staff #3's shift is compared against both of Staff #1's two remaining ranges of alone time. Staff #3's shift doesn't overlap any part of the first subarray, but it does in the second. This means the second range of time is then replaced to effectively "punch out" the overlap in shift time.
    $pieces_of_shift=[[660,780],[900,1020],[1140,1260]];
  • This results in Staff #1's shift have 3 periods of "alone" time: 660 to 780, 900 to 1020, and 1140 to 1260. These 3 ranges of alone time (2hrs each) yields 6 hours of total solo work or 360 minutes.

Here is a demo with additional comments.


If there is a high probability or high volume of duplicate shifts in a particular batch, total iterations inside of whittle() can be reduced by writing $colleague_shifts=array_map('unserialize', array_unique(array_map('serialize', $shifts))) before the first foreach() loop.

For that matter, the same multi-functional approach could be used to shortcut several duplicate shifts before the call of foreach($shifts...), but I have chosen not to implement that approach because it may not be worth the convolution.



回答2:

Got it!

It took some time but I found a solution.
Managed to find a solution to mickmacks test case.
Here is a ten person case and it seems to hold up for that too.

<?php
$staff = array(1,2,3,4,5,6,7,8,9,10);
$start = array("11:00", "13:00", "17:00", "17:00", "11:00", "13:30", "16:50", "18:30","17:00", "11:00");
$end = array("21:00", "15:00", "19:00", "19:30", "11:30", "15:10", "18:45", "19:45", "19:00", "11:30");

// Add staff number to end of time ex 11:00 => 11:00#2
For($i=0; $i<count($start);$i++){
    $start[$i] .= "#" . $staff[$i];
    $end[$i] .= "#" . $staff[$i];

}
$t = array_merge($start,$end); // create one long array with all in and out times
sort($t);
//var_dump($t);
// Multisport is needed to get all arrays in time order as reference
array_multisort($start, $end, $staff);

// Find first start time (11:00) and slice array thwre, build string
$test = implode(PHP_EOL,array_slice($t, array_search($start[0], $t)));

// Find the times before first start (night end times) and add them last in string
$test .= PHP_EOL . implode(PHP_EOL,array_slice($t, 0,array_search($start[0], $t)));
$times = explode(PHP_EOL, $test); // explode to make it array again
 // Var_dump($times);

$WhoIsInDaHouse = array("dummy"); // add a dummy variable since 0=false in later if
$j=0;
for($i=0; $i<count($times);$i++){
    //echo $times[$i] ." " . $i ."\n";
    if($times[$i]){
        $TimePerson = explode("#", $times[$i]);
        $Time = $TimePerson[0];
        $person = $TimePerson[1];


        $inout = array_search($person, $WhoIsInDaHouse); //is person in house and about to leave?
        If($inout != false){ //if person enter work false, if true: key of person leaving in $WhoIsInDaHouse
            //Here $person is leaving work
            Unset($WhoIsInDaHouse[$inout]);

            If(count($WhoIsInDaHouse) == 2){ // someone will now be alone since we have a dummy
                $Alone[$j]["start"] = $Time;
                $Alone[$j]["who"] = array_slice($WhoIsInDaHouse, -1)[0];
            }elseif(count($WhoIsInDaHouse) == 1 && $prevcount == 2){
                // Only dummy left
                $Alone[$j]["end"] = $Time;
                $Alone[$j]["duration"] = strtotime($Alone[$j]["end"])-strtotime($Alone[$j]["start"]);
                $j++;
            }
        }Else{
            // Here person enters work
            $WhoIsInDaHouse[] = $person;

            If(count($WhoIsInDaHouse) == 2){ // someone is entering alone
                $Alone[$j]["start"] = $Time;
                $Alone[$j]["who"] = $person;
            }elseif(count($WhoIsInDaHouse)>2 && $prevcount == 2){ // not alone anymore
                $Alone[$j]["end"] = $Time;
                $Alone[$j]["duration"] = strtotime($Alone[$j]["end"])-strtotime($Alone[$j]["start"]);
                $j++;
            }
        }
        $prevcount = count($WhoIsInDaHouse);
    }
}
foreach($Alone as $key => &$loner){
    if($loner["duration"]==0) unset($Alone[$key]);
}
Var_dump($Alone);

And see the beauty run https://3v4l.org/bT2bZ

It took me a long time to figure out I needed a dummy. Who knew a dummy could be useful?