Advertisement
rfv123

TimeSlotRange (SO Question 40505794)

Nov 21st, 2016
202
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 12.35 KB | None | 0 0
  1.  
  2. /* ---------------------------------------------------------------------------------
  3.  * Process One range of TimeSlots
  4.  */
  5.  
  6. /*
  7.  *  Requirements:
  8.  *  
  9.  *    Input:  
  10.  *        1) One date range that will contain the optimal list of timeslots
  11.  *        2) list of 'required' dates    - these are fixed and will always be in the final list
  12.  *        3) A list of 'candidate' dates - fit as many of these in as we can.
  13.  *        
  14.  *    Output:
  15.  *      1) A 'final list of 'none-overlapping' dates
  16.  *         Where:
  17.  *           a) will contain all the 'required dates'
  18.  *           b) as many candidates as will fit
  19.  *           c) No timeslot will have overlapping dates.  
  20.  *
  21.  *    Note: There is no validation done in this class.
  22.  *          see TimeSlotList for validation / filtering of the range  
  23.  */  
  24.  
  25. class TimeSlotRange
  26. {
  27.     /**
  28.     *  All the candidate timeslots that don't overlap with any 'required' date
  29.     *
  30.     * @var array $candidates
  31.     */
  32.     private $candidates = array();    
  33.  
  34.     /**
  35.     * Any timeslot record we didn't use.
  36.     *
  37.     * @var array $unused
  38.     */
  39.     private $unused = array();
  40.        
  41.     /**
  42.     * List of dates that must be included in the 'final' list.
  43.     * There must not be any overlapping ranges
  44.     *  
  45.     * @var array $required
  46.     */    
  47.     private $required = array();
  48.  
  49.     /**
  50.     * Timeslot range all dates must be between
  51.     *
  52.     * @var array $ranges
  53.     */    
  54.     private $range = null;
  55.  
  56.     /**
  57.     * Ordered list of 'none overlapping' dates from the Required and candidates
  58.     *
  59.     * @var array $final
  60.     */    
  61.     private $final = array();
  62.    
  63.    
  64.     /**
  65.     * Current List of paths with associated score
  66.     *
  67.     * The top one is the current best one
  68.     */
  69.     private $currentPath = array();
  70.  
  71.  
  72.     /**
  73.     * Check if candidate overlaps any of the dates in the list
  74.     *
  75.     * @param array $candidate
  76.     * @param array $whenList  -- list of non-overlapping dates
  77.     *
  78.     * @return boolean  true if overlaps
  79.     */
  80.     public static function isOverlapAny(ITimeSlot $candidate, $whenList)
  81.     {
  82.         foreach ((array) $whenList as $when) {
  83.             if ($candidate->isOverlap($when)) { // ignore it
  84.                return true;
  85.             }
  86.         }
  87.         return false;
  88.     }  
  89.  
  90.    
  91.     /**
  92.     * These are the date lists.
  93.     * They will NOT be filtered or validated. see TimeSlotList for that
  94.     *
  95.     * @range ITimeSlot $range
  96.     * @param array     $requiredDates
  97.     * @param array     $candidateDates
  98.     * @return void
  99.     */
  100.     public function __construct(ITimeSlot $range,
  101.                                 array $requiredDates = array(),
  102.                                 array $candidateDates = array())
  103.     {
  104.         if (!empty($range)) {
  105.             $this->setRange($range);
  106.         }
  107.  
  108.         if (!empty($requiredDates)) {
  109.             $this->setRequired($requiredDates);
  110.         }
  111.        
  112.         if (!empty($candidateDates)) {
  113.             $this->setCandidates($candidateDates);
  114.         }
  115.     }
  116.  
  117.     /**
  118.     * Add the candidates to the final list
  119.     *
  120.     *   Known conditions:
  121.     *     o  Both lists are in start date order
  122.     *     o  No candidates overlap with any Required date
  123.     *     o  The candidates may overlap with each other - Hmm... need to check...
  124.     *
  125.     *  Note: The '$this->isOverlapsAny' method - as it is used a lot will be expensive (O(n^2))
  126.     *        I can think of ways to reduce that - will happen when it is  refactored ;-/
  127.     *
  128.     *        This version uses the 'brute force'path search.
  129.     *  
  130.     * @return array
  131.     */
  132.     public function generateList()
  133.     {
  134.         $whenList = $this->bestFitDates($this->range,
  135.                                         $this->required,
  136.                                         $this->candidates);            
  137.                      
  138.  
  139.         // sort final by start times - makes it easy to reason about
  140.         $this->sortStartTime($whenList);
  141.              
  142.         $this->final = $whenList;
  143.        
  144. //        $this->unused = array_diff($this->final, $this->candidates);
  145.  
  146.         // sort final by start times - makes it easy to reason about
  147. //        $this->sortStartTime($this->unused);
  148.              
  149.         return $this->final;      
  150.     }
  151.  
  152.     /**
  153.     * Build and walk a 'decision tree'. This will be 'interesting' ;-/
  154.     *
  155.     * @return array list of timeslots that will fit in the range
  156.     */
  157.     public function bestFitDates(ITimeSlot $range,
  158.                                  array $required,
  159.                                  array $candidates)
  160.     {
  161.           $rangeDuration = $range['duration'];
  162.          
  163.           $totalRequiredDuration = 0;
  164.           foreach ($required as $when) {
  165.               $totalRequiredDuration += $when['duration'];
  166.           }
  167.        
  168.           $this->currentPath = array();
  169.          
  170.           $bestPath = array('path' => $required, 'score' => $totalRequiredDuration);
  171.           array_push($this->currentPath, $bestPath);
  172.          
  173.           $this->maxDuration($required,
  174.                              $candidates,
  175.                              count($candidates) - 1,
  176.                              $rangeDuration - $totalRequiredDuration,
  177.                              $totalRequiredDuration);
  178.                  
  179.                                          
  180.           return array_pop($this->currentPath)['path']; // best path                                        
  181.     }
  182.  
  183.     /**
  184.     * The algorithn creates and walks a 'binary decision tree' using 'unused' and 'used' branches
  185.     * for the current timeslot.
  186.     *
  187.     * The method is to
  188.     *     o create a list of the timeslots by adding and removing one of the candiates
  189.     *       at a time.
  190.     *
  191.     *     o Keep a score for the list which is just the sum of all the durations of all the
  192.     *       current timeslots in the list
  193.     *
  194.     *     o if the current score is higher than any found so far then record this as the 'best' list
  195.     *        see: $this->currentPath
  196.     *
  197.     *
  198.     *
  199.     * @param array     $currentWhenList    The current complete list of timeslots we are checking
  200.     * @param array     $candidates         The complate list of candidate timeslots to use
  201.     * @param integer   $candidateIdx       The index of the current candidate we are checking
  202.     * @param integer   $availableDuration  The total amount of time that is available top use
  203.     * @param integer   $totalDuration      The total duration of the path so far (score)
  204.     * @param string    $whichPath          Debugging - show which node branch currently being processed
  205.     *
  206.     * @return boolean  True when reached a leaf node, false when 'cannot continue'
  207.     */
  208.     public function maxDuration($currentWhenList,
  209.                                 $candidates,
  210.                                 $candidateIdx,
  211.                                 $availableDuration,
  212.                                 $totalDuration,
  213.                                 $whichPath = '')
  214.     {              
  215.         // check whether current path has higher score than current
  216.         $topPath = end($this->currentPath);
  217.         if ($totalDuration > $topPath['score']) { // save it
  218.  
  219.             $bestPath = array('path' => $currentWhenList, 'score' => $totalDuration);
  220.             array_push($this->currentPath, $bestPath);
  221.         }
  222.        
  223.         if ($candidateIdx < 0) { // no more candidates
  224.            return true;  
  225.         }            
  226.        
  227.        
  228.         /*
  229.          * Unused path  - don't add the current 'candidate' timeslot
  230.          *
  231.          *                Always check the 'unused' branch first  
  232.          */
  233.         $this->maxDuration($currentWhenList,
  234.                            $candidates,
  235.                            $candidateIdx - 1,
  236.                            $availableDuration,
  237.                            $totalDuration,
  238.                            'unused');
  239.          
  240.         /*
  241.          * Used path  - add the current candidate timeslot...
  242.          *            
  243.          *              o It may take us over the limit of duration
  244.          *              o It may overlap with others already in the list!  
  245.          */
  246.  
  247.         // assume we are going to use it:
  248.         // Calculate all the new stuff as we are going to use the candidate
  249.         $newAvailableDuration = $availableDuration - $candidates[$candidateIdx]['duration'];
  250.         $newTotalDuration  = $totalDuration + $candidates[$candidateIdx]['duration'];
  251.  
  252.         if ($newAvailableDuration < 0) { // it will not fit
  253.             return false;
  254.         }
  255.  
  256.         // will it overlap with any existing timeslots already in the list?
  257.         if ($this->isOverlapAny($candidates[$candidateIdx], $currentWhenList)) {
  258.             return false;
  259.         }
  260.  
  261.  
  262.         // add it to the current candidate list and recurse to check the other candidates
  263.  
  264.         $key = $candidates[$candidateIdx]->getKey();
  265.         $currentWhenList[$key] = $candidates[$candidateIdx]; // add to list
  266.        
  267.         $this->maxDuration($currentWhenList,       // list of candidates being tried
  268.                            $candidates,
  269.                            $candidateIdx - 1,      // ensure we check the next candidate !important
  270.                            $newAvailableDuration,
  271.                            $newTotalDuration,
  272.                            'used ');
  273.                    
  274.         unset($currentWhenList[$key]); // remove it from the list!                                
  275.                                            
  276.         return true;
  277.     }
  278.    
  279.  
  280.     /**
  281.     * Used to check if a timeslot will completely fit in this range
  282.     *
  283.     * @param ITimeSlot $when
  284.     *
  285.     * @return boolean true when inside
  286.     */
  287.     public function isInsideRange(ITimeSlot $when)
  288.     {
  289.         return $this->range->isInside($when);
  290.     }
  291.  
  292.     /**
  293.     * date range
  294.     *
  295.     * @param array $range
  296.     */
  297.     public function setRange(ITimeSlot $range)
  298.     {
  299.         $this->range = $range;
  300.     }
  301.  
  302.  
  303.     /**
  304.     * date range and sort them
  305.     *
  306.     * @param array $requiredDates
  307.     */
  308.     public function setRequired(array $dates)
  309.     {
  310.         foreach ($dates as $date) {
  311.             $date->setIsRequired();
  312.             $this->required[] = $date;
  313.         }
  314.        
  315.         // sort final by start times - makes it easy to reason about
  316.         $this->sortStartTime($dates);
  317.         $this->required = $dates;
  318.     }
  319.  
  320.     /**
  321.     * setter for candidates
  322.     *
  323.     * @param array $candidateDates
  324.     *
  325.     * @return void;
  326.     */
  327.     public function setCandidates(array $dates)
  328.     {
  329.         $this->sortStartTime($dates);
  330.  
  331.         $this->candidates = $dates;
  332.        
  333.     }
  334.  
  335.     public function sortEndTime(&$timeSlots)
  336.     {        
  337.         // sort by end times - makes it easy to reason about
  338.         usort($timeSlots,
  339.               function ($when1, $when2) {
  340.                     $order = $when1['endTime'] - $when2['endTime'];
  341.                     if ($order === 0) { // want the largest first
  342.                         return $when2['duration'] - $when1['duration'];
  343.                     }
  344.                     return $order;
  345.               });
  346.              
  347.         return;      
  348.     }
  349.  
  350.     public function sortStartTime(&$timeSlots)
  351.     {        
  352.         // sort by end times - makes it easy to reason about
  353.         usort($timeSlots,
  354.               function ($when1, $when2) {
  355.                     $order = $when1['startTime'] - $when2['startTime'];
  356.                     if ($order === 0) { // want the largest first
  357.                         return $when2['duration'] - $when1['duration'];
  358.                     }
  359.                     return $order;
  360.               });
  361.              
  362.         return;      
  363.     }
  364.  
  365.     /*
  366.      *  `Getters` so you can see what happened
  367.      */
  368.     public function getRequired()   { return $this->required; }
  369.     public function getRange()      { return $this->range; }
  370.     public function getUnused()     { return $this->unused; }
  371.     public function getCandidates() { return $this->candidates; }
  372.     public function getFinal()      { return $this->final; }
  373.    
  374.     /**
  375.     * properties - for those of us that like them
  376.     */
  377.     public function __get($name)
  378.     {
  379.         if (property_exists($this, $name)) {
  380.             return $this->$name;
  381.         }        
  382.         return null;
  383.     }
  384. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement