Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 89
WF_template
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 12
1892.00
0.00% covered (danger)
0.00%
0 / 89
 __construct
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 3
 setup
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 11
 sortByEst
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 5
 validate
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 10
 set_next
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 15
 prevNextAll_deduce
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 4
 prevAll_deduce
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 5
 nextAll_deduce
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 5
 remove_fakeStart
0.00% covered (danger)
0.00%
0 / 1
12.00
0.00% covered (danger)
0.00%
0 / 4
 cycles_detect
0.00% covered (danger)
0.00%
0 / 1
20.00
0.00% covered (danger)
0.00%
0 / 5
 earlyTimes_calculate
0.00% covered (danger)
0.00%
0 / 1
42.00
0.00% covered (danger)
0.00%
0 / 9
 lateTimes_calculate
0.00% covered (danger)
0.00%
0 / 1
30.00
0.00% covered (danger)
0.00%
0 / 13
<?php
namespace ia\Work\Workflow;
use function array_combine;
use function array_key_exists;
use Exception;
use function implode;
use function uasort;
/**
 * Workflow template: define tiempos estandard y sequencia de actividades.
 *
 * Valida y calcula agregando est,lst,eet,let, slack, next, prev activity, prevAll, nextAll.
 * Main usage: get user input for wf template: validate, acompleta, para guardar.
 *
 * est      earliest start time, int nĂºmero de periodos: dias/horas
 * lst      latest start time
 * eet      earliest end time
 * let      latest end time
 * slack    holgura
 *
 *
 * plan=>[
 *  eet=>ya es fecha
 *
 *
 * ]
 *
 * rule invalid id "s\ttart"
 *
 * activityList =
        * [
            * 'a'=>['duration'=>1, 'label'=>'Act: a', 'myData'=>1],
            * 'b'=>['duration'=>3],
            * 'c'=>['duration'=>4,'prev'=>['a','b'=>'b']],
            * 'd'=>['duration'=>5,'prev'=>['c']],
            * 'f'=>['duration'=>4,'prev'=>['b']],
            * 'e'=>['duration'=>5,'prev'=>['d']],
        * ];
 *
 *
 *
 *
 *
 * @version 0.0.1
 */
class WF_template  {
    use wfx;
//https://www.youtube.com/watch?v=WHkf4bE5MrE
//
    public const FAKE_START = "st\tart";
    // sprotected $durationConstant;
    /**
     * WF_template constructor.
     * @param array $activityList
     * @throws Exception
     */
    public function __construct(array $activityList) { //, $durationOffset = 0
        $this->activityList = $activityList;
        //$this->durationConstant = $durationOffset;
        //$this->durationConstant = 0;
            $this->setup();
    }
    /**
     * @throws Exception
     */
    private function setup() {
        $this->validate();
        $start = $this->set_next();
        $this->prevNextAll_deduce();
        $cycles = $this->cycles_detect();
        if(!empty($cycles)) {
            throw new Exception("Cycles detected for activities: ". implode(", ", $cycles), self::$WF_ERROR_CYCLE);
        }
        $this->earlyTimes_calculate($start);
        $this->lateTimes_calculate($start);
        uasort($this->activityList, array($this, 'sortByEst') );
        $this->remove_fakeStart();
    }
    /**
     * @param array $a
     * @param array $b
     * @return int
     */
    final protected function sortByEst($a, $b) {
        if($a['est'] === $b['est']) {
            if($a['slack'] === $b['slack']) {
                return $a['duration'] - $b['duration'];
            }
            return $a['slack'] - $b['slack'];
        }
        return $a['est'] - $b['est'];
    }
// [ activity_id=>[prev=>['a','b'],label=>'activity name'] ]
    /**
     * @throws Exception
     */
    private function validate() {
        foreach($this->activityList as $activity_id => &$act) {
            $act = array_merge(
                 [ // fill if not in act
                    'id'=>$activity_id,
                    'label'=>$activity_id,
                    'prev' => [],           // Inmediatly previous activitie(s)
                 ],
                $act,
                [ // fill and replace if in act
                    'est'=>null,    // Earliest start time
                    'lst'=>null,    // Latest start time
                    'eet'=>null,    // Earleist end time
                    'let'=>null,    // Latest end time
                    'prevAll'=>[],  // All activities before this one
                    'next'=>[],     // Inmediatly following activitie(s)
                    'nextAll'=>[],  // All activities that follow this one
                    'slack'=>0      // lag time, diff between eet and let
                ]
            );
            if(!isset($act['duration']) || !is_numeric($act['duration']) || $act['duration'] < 0) {
                throw new Exception("Missing or invalid duration in activity $act[label]", self::$WF_ERROR_DURATION);
            }
            // reformat prev from ['a', 'b'] to ['a'=>'a', 'b'=>'b']
            $act['prev'] = array_combine($act['prev'], $act['prev']);
        }
    }
    /**
     * @return array
     * @throws Exception
     */
    private function set_next() {
        $start = [
            'id' => self::FAKE_START,
            'duration' => 0, // How long the activity lasts
            'est' => 0, // Earliest start time
            'lst' => 0, // Latest start time
            'eet' => 0, // Earliest end time
            'let' => 0, // Latest end time
            'prev' => [], // Inmediatly previous activitie(s)
            'next' => [], // Inmediatly following activitie(s)
        ];
        foreach($this->activityList as $activity_id => &$act) {
            if(empty($act['prev'])) {
                $act['prev'][self::FAKE_START] = self::FAKE_START;
                $start['next'][$activity_id] = $activity_id;
            } else {
                foreach($act['prev'] as $prev) {
                    if(empty($this->activityList[$prev])) {
                        throw new Exception("Unknown prev activity id: $prev in Activity: $act[label]", self::$WF_ERROR_ACTIVITY_NOT_FOUND);
                    }
                    $this->activityList[$prev]['next'][$activity_id] = $activity_id;
                }
            }
        }
        return $start;
    }
    /**
     *
     */
    private function prevNextAll_deduce() {
        foreach($this->activityList as $activity_id => &$act) {
           $this->prevAll_deduce($act['prev'], $act['prevAll']);
           $this->nextAll_deduce($act['next'], $act['nextAll']);
        }
    }
    /**
     * @param array $prev
     * @param array $prevAll
     */
    private function prevAll_deduce(array $prev, array &$prevAll) {
        foreach($prev as $prev_id) {
            if($prev_id !== self::FAKE_START && empty($prevAll[$prev_id])) {
                $prevAll[$prev_id] = $prev_id;
                $this->prevAll_deduce($this->activityList[$prev_id]['prev'], $prevAll);
            }
        }
    }
    /**
     * @param array $next
     * @param array $nextAll
     */
    private function nextAll_deduce(array $next, array &$nextAll) {
        foreach($next as $next_id) {
            if(empty($nextAll[$next_id])) {
                $nextAll[$next_id] = $next_id;
                $this->nextAll_deduce($this->activityList[$next_id]['next'], $nextAll);
            }
        }
    }
    /**
     *
     */
    private function remove_fakeStart() {
        foreach($this->activityList as &$act) {
            if(isset($act['prev'][self::FAKE_START])) {
                unset($act['prev'][self::FAKE_START]);
            }
        }
    }
    /**
     * @return array
     */
    private function cycles_detect() {
        $cycles = [];
        foreach($this->activityList as $activity_id => $act) {
            if(array_key_exists($activity_id, $act['prevAll']) || array_key_exists($activity_id, $act['nextAll'])) {
                $cycles[] = $act['label'];
            }
        }
        return $cycles;
    }
    /**
     * @param array $fromActivity
     */
    private function earlyTimes_calculate(array $fromActivity) {
        foreach($fromActivity['next'] as $activity_id)  {
            $node = &$this->activityList[$activity_id] ;
            foreach($node['prev'] as $prev) {
                $activity = empty($this->activityList[$prev]) ? ['eet'=>0,] : $this->activityList[$prev]; // empty es start
                if($node['est'] === null || $node['est'] < $activity['eet']) {
                    $this->activityList[$activity_id]['est'] = $activity['eet'];
                }
                $this->activityList[$activity_id]['eet'] = $this->activityList[$activity_id]['est'] + $node['duration']; // - $this->durationConstant;
                $this->earlyTimes_calculate($this->activityList[$activity_id]);
            }
        }
    }
    /**
     * @param array $fromActivity
     */
    private function lateTimes_calculate(array &$fromActivity) {
        if(count($fromActivity['next']) === 0) {
            $fromActivity['let'] = $fromActivity['eet'];
            $fromActivity['lst'] = $fromActivity['let'] - $fromActivity['duration']; // + $this->durationConstant;
            $fromActivity['slack'] = $fromActivity['eet'] - $fromActivity['let'];
            return;
        }
        foreach($fromActivity['next'] as $next_id) {
            $this->lateTimes_calculate($this->activityList[$next_id]);
            $node = $this->activityList[$next_id];
            if($fromActivity['let'] === null || $fromActivity['let'] > $node['lst']){
                 $fromActivity['let'] = $node['lst'];
            }
            $fromActivity['lst'] = $fromActivity['let'] - $fromActivity['duration']; // + $this->durationConstant;
            $fromActivity['slack'] = $fromActivity['let'] - $fromActivity['eet'];
        }
    }
}