Completed
Pull Request — master (#28)
by claudio
05:35
created

Optimise::fillTimeSlots()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.6667
cc 3
eloc 6
nc 3
nop 4
crap 3
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: claudio
5
 * Date: 12/12/15
6
 * Time: 15.41
7
 */
8
9
namespace plunner\Console\Commands\Optimise;
10
11
use Illuminate\Console\Scheduling\Schedule;
12
use plunner\company;
13
use plunner\Events\Optimise\ErrorEvent;
14
15
/**
16
 * Class Optimise
17
 * @author Claudio Cardinale <[email protected]>
18
 * @copyright 2015 Claudio Cardinale
19
 * @version 1.0.0
20
 * @package plunner\Console\Commands\Optimise
21
 */
22
class Optimise
23
{
24
    //TODO insert MAX timeslots limit during meeting creation
25
    //TODo max timeslots can be an environment var
26
    const TIME_SLOT_DURATION = 900; //seconds -> 15 minutes
27
28
    private $max_time_slots = 20; //max duration of a meeting in term of timeslots //20
29
    private $time_slots = 672; //total amount of timeslots that must be optimised -> one week 4*24*7 = 672
30
31
    //TODO timezone
32
    //TODO fix here
33
    /**
34
     * @var \DateTime
35
     */
36
    private $startTime;
37
    /**
38
     * @var \DateTime
39
     */
40
    private $endTime;
41
42
    /**
43
     * @var Company
44
     */
45
    private $company;
46
47
    /**
48
    * @var Schedule laravel schedule object needed to perform command in background
49
    */
50
    private $schedule;
51
52
    /**
53
     * @var \Illuminate\Contracts\Foundation\Application;
54
     */
55
    private $laravel;
56
57
    /**
58
     * @var Solver
59
     */
60
    private $solver = null;
61
62
    //TODO clone
63
    //TODO to_string
64
65
    /**
66
     * Optimise constructor.
67
     * @param company $company
68
* @param Schedule $schedule
69
     * @param \Illuminate\Contracts\Foundation\Application $laravel
70
     */
71 2
    public function __construct(company $company, Schedule $schedule, \Illuminate\Contracts\Foundation\Application $laravel)
72
    {
73 2
        $this->company = $company;
0 ignored issues
show
Documentation Bug introduced by
It seems like $company of type object<plunner\Company> is incompatible with the declared type object<plunner\Console\Commands\Optimise\Company> of property $company.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
74 2
        $this->schedule = $schedule;
75 2
        $this->laravel = $laravel;
76
77 2
        $this->setStartTime((new \DateTime())->modify('next monday'));
78 2
    }
79
80
81
    /**
82
     * @param \DateTime $startTime
83
     */
84 2
    public function setStartTime(\DateTime $startTime)
85
    {
86 2
        $this->startTime = clone $startTime;
87 2
        $this->endTime = clone $this->startTime;
88 2
        $this->endTime->add(new \DateInterval('PT'.(($this->max_time_slots+$this->time_slots)*self::TIME_SLOT_DURATION).'S'));
89 2
    }
90
91
    /**
92
     * @return int
93
     */
94
    public function getMaxTimeSlots()
95
    {
96
        return $this->max_time_slots;
97
    }
98
99
    /**
100
     * @param int $max_time_slots
101
     */
102 2
    public function setMaxTimeSlots($max_time_slots)
103
    {
104 2
        $this->max_time_slots = $max_time_slots;
105 2
    }
106
107
    /**
108
     * @return int
109
     */
110 2
    public function getTimeSlots()
111
    {
112 2
        return $this->time_slots;
113
    }
114
115
    /**
116
     * @param int $time_slots
117
     */
118 2
    public function setTimeSlots($time_slots)
119
    {
120 2
        $this->time_slots = $time_slots;
121 2
    }
122
123
124
    /**
125
     * @return Company
126
     */
127
    public function getCompany()
128
    {
129
        return $this->company;
130
    }
131
132
    /**
133
     * @param Company $company
134
     */
135
    public function setCompany($company)
136
    {
137
        $this->company = $company;
138
    }
139
140
    /**
141
     * @return Solver
142
     */
143 2
    public function getSolver()
144
    {
145 2
        return $this->solver;
146
    }
147
148
149
    /**
150
     * @return Optimise
151
     */
152 2
    public function optimise()
153
    {
154 2
        try {
155 2
            $solver = new Solver($this->schedule, $this->laravel);
0 ignored issues
show
Compatibility introduced by
$this->laravel of type object<Illuminate\Contra...Foundation\Application> is not a sub-type of object<Illuminate\Foundation\Application>. It seems like you assume a concrete implementation of the interface Illuminate\Contracts\Foundation\Application to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
156 2
            $solver = $this->setData($solver);
157 2
            $solver = $solver->solve();
158 2
            $this->solver = $solver;
159 2
        }catch(\Exception $e)
160
        {
161
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
162
            throw new OptimiseException('Optimising error', 0, $e);
163
            //TODO catch specif exception
164
        }
165 2
        return $this;
166
    }
167
168
    /**
169
     * @return Optimise
170
     */
171 2
    public function save()
172
    {
173 2
        if(!($this->solver instanceof Solver)) {
174
            \Event::fire(new ErrorEvent($this->company, 'solver is not an instace of Solver'));
175
            throw new OptimiseException('solver is not an instance of Solver');
176
            return;
0 ignored issues
show
Unused Code introduced by
return; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
177
        }
178
        //TODO check results before save them
179
180
        try {
181 2
            $this->saveMeetings($this->solver);
182 2
            $this->saveEmployeesMeetings($this->solver);
183 2
        }catch(\Exception $e)
184
        {
185
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
186
            throw new OptimiseException('Optimising error', 0, $e);
187
            //TODO catch specif exception
188
        }
189 2
        return $this;
190
    }
191
192
    //TODO fix php doc with exceptions
193
194
    /**
195
     * @param Solver $solver
196
     */
197 2
    private function saveMeetings(Solver $solver)
198
    {
199 2
        $meetings = $solver->getYResults();
200 2
        foreach($meetings as $id=>$meeting){
201 2
            $meetingO = \plunner\Meeting::findOrFail($id);//TODO catch error
202 2
            $meetingO->start_time = $this->toDateTime(array_search('1', $meeting));
203 2
            $meetingO->save();
204 2
        }
205 2
    }
206
207
    /**
208
     * @param Solver $solver
209
     */
210 2
    private function saveEmployeesMeetings(Solver $solver)
211
    {
212 2
        $employeesMeetings = $solver->getXResults();
213 2
        foreach($employeesMeetings as $eId =>$employeeMeetings)
214
        {
215 2
            $employee = \plunner\Employee::findOrFail($eId);
216 2
            $employeeMeetings = collect($employeeMeetings);
217
            $employeeMeetings = $employeeMeetings->filter(function ($item) {
218 2
                return $item == 1;
219 2
            });
220 2
            $employee->meetings()->attach($employeeMeetings->keys()->toArray());
221 2
        }
222 2
    }
223
224
225
    /**
226
     * @param Solver $solver
227
     * @return Solver
228
     * @throws OptimiseException
229
     */
230 2
    private function setData(Solver $solver)
231
    {
232
        //TODO get avalability only of this week
233
234 2
        $solver = $this->setTimeSlotsSolver($solver);
235 2
        $solver = $this->setUsers($solver);
236 2
        $solver = $this->setAllMeetingsInfo($solver);
237 2
        $solver = $this->setUserAvailability($solver);
238 2
        $solver = $this->setUsersMeetings($solver);
239 2
        return $solver;
240
    }
241
242
    /**
243
     * @param Solver $solver
244
     * @return Solver
245
     * @throws OptimiseException
246
     */
247 2
    private function setTimeSlotsSolver(Solver $solver)
248
    {
249 2
        return $solver->setTimeSlots($this->time_slots)->setMaxTimeSlots($this->max_time_slots);
250
    }
251
252
    /**
253
     * @param Solver $solver
254
     * @return Solver
255
     */
256 2
    private function setUsers(Solver $solver)
257
    {
258
        //since we consider busy timeslots, we need to get all users
259 2
        $users = $this->company->employees->pluck('id')->toArray();
260 2
        return $solver->setUsers($users);
261
    }
262
263
    /**
264
     * @param Solver $solver
265
     * @return Solver
266
     */
267 2
    private function setAllMeetingsInfo(Solver $solver)
268
    {
269
        /**
270
         * @var $meetings \Illuminate\Support\Collection
271
         */
272 2
        $meetings = collect($this->company->getMeetingsTimeSlots($this->startTime, $this->endTime));
273
        $timeslots = $meetings->groupBy('id')->map(function($item) { //convert timeslots
274 2
                return $this->timeSlotsConverter($item);
275 2
            });
276 2
        return $solver->setMeetings($timeslots->keys()->toArray())
277 2
            ->setMeetingsDuration($meetings->pluck('duration','id')->toArray())
278 2
            ->setMeetingsAvailability(self::getAvailabilityArray($timeslots, $this->time_slots));
279
    }
280
281
    /**
282
     * @param Solver $solver
283
     * @return Solver
284
     * @throws OptimiseException
285
     */
286 2
    private function setUserAvailability(Solver $solver)
287
    {
288
        /**
289
         * @var $users \Illuminate\Support\Collection
290
         */
291 2
        $users = collect($this->company->getEmployeesTimeSlots($this->startTime, $this->endTime));
292
        $timeslots = $users->groupBy('id')->map(function($item) { //convert timeslots
293 2
                return $this->timeSlotsConverter($item);
294 2
            });
295 2
        return $solver->setUsersAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, false));
296
    }
297
298
    /**
299
     * @param Solver $solver
300
     * @return Solver
301
     * @throws OptimiseException
302
     */
303 2
    private function setUsersMeetings(Solver $solver)
304
    {
305 2
        $users = $solver->getUsers();
306 2
        $meetings = $solver->getMeetings();
307
        /**
308
         * @var $usersMeetings \Illuminate\Support\Collection
309
         */
310 2
        $usersMeetings = collect($this->company->getUsersMeetings($users, $meetings))->groupBy('employee_id');
311
312 2
        return $solver->setUsersMeetings(self::getUsersMeetingsArray($users, $meetings, $usersMeetings));
313
    }
314
315
    /**
316
     * @param array $users
317
     * @param array $meetings
318
     * @param \Illuminate\Support\Collection $usersMeetings
319
     * @return array
320
     */
321 2
    static private function getUsersMeetingsArray($users, $meetings, \Illuminate\Support\Collection $usersMeetings)
322
    {
323 2
        $ret = [];
324 2
        foreach($users as $user)
325
        {
326 2
            $usersMeetingsTmp = $usersMeetings->get($user);
327 2
            foreach($meetings as $meeting){
328 2
                if($usersMeetingsTmp->contains('meeting_id', $meeting)){
329 2
                    $ret[$user][$meeting] = 1;
330 2
                }else{
331 2
                    $ret[$user][$meeting] = 0;
332
                }
333 2
            }
334 2
        }
335
336 2
        return $ret;
337
    }
338
339
    private function timeSlotsConverter($item)
340
    {
341 2
        return $item->each(function($item2){
342 2
            $item2->time_start = $this->toTimeSlot($item2->time_start);
343 2
            $item2->time_end = $this->toTimeSlot($item2->time_end);
344 2
            return $item2;
345
            //TODO try catch
346 2
        });
347
    }
348
349
    /**
350
     * @param \Illuminate\Support\Collection $timeSlots
351
     * @param bool|true $free if true the array is filled with 1 for timeslots values else with 0 for timeslots values
352
     * @return array
353
     */
354 2
    static private function getAvailabilityArray(\Illuminate\Support\Collection $timeSlots, $timeslotsN, $free=true)
355
    {
356 2
        $ret = [];
357 2
        foreach($timeSlots as $id=>$timeSlots2)
358
        {
359 2
            $ret = self::fillTimeSlots($ret, $id, $timeSlots2, $free?'1':'0');
360 2
            $ret = self::fillRow($ret, $id, $timeslotsN, $free?'0':'1');
361 2
        }
362
363 2
        return $ret;
364
    }
365
366
    /**
367
     * @param array $array
368
     * @param int $id
369
     * @param \Illuminate\Support\Collection $timeSlots
370
     * @param string $fill
371
     * @return array
372
     */
373 2
    static private function fillTimeSlots(array $array, $id, \Illuminate\Support\Collection $timeSlots, $fill = '0')
374
    {
375 2
        foreach($timeSlots as $timeSlot) {
376 2
            if(!isset($array[$id]))
377 2
                $array[$id] = [];
378 2
            $array[$id] = self::arrayPadInterval($array[$id], $timeSlot->time_start, $timeSlot->time_end, $fill);
379 2
        }
380 2
        return $array;
381
    }
382
383
    /**
384
     * @param array $array
385
     * @param int $id
386
     * @param string $fill
387
     * @return array
388
     */
389 2
    static private function fillRow(array $array, $id, $until, $fill = '0')
390
    {
391 2
        for($i = 1; $i <= $until; $i++){
392 2
            if(!isset($array[$id][$i]))
393 2
                $array[$id][$i] = $fill;
394 2
        }
395
396 2
        return $array;
397
    }
398
399
    /**
400
     * @param array $array
401
     * @param int $from
402
     * @param int $to
403
     * @param string $pad
404
     * @return array
405
     */
406 2
    static private function arrayPadInterval(array $array, $from, $to, $pad = '0')
407
    {
408 2
        for($i = $from; $i<$to; $i++)
409 2
            $array[$i] = $pad;
410 2
        return $array;
411
    }
412
413
414
    /**
415
     * @param mixed $time
416
     * @return int
417
     * @throws OptimiseException
418
     */
419 2
    private function toTimeSlot($time)
420
    {
421 2
        $dateTime = new \DateTime($time);
422 2
        $diff = $dateTime->diff($this->startTime);
423 2
        $diff = explode(':',$diff->format('%R:%d:%h:%i:%s'));
424 2
        $diff = $diff[1]*86400 + $diff[2]*3600 + $diff[3]*60 + $diff[4];
425
        //if($diff[0] != '-' && $diff != 0)
426
          //  throw new OptimiseException('timeslot time <= startTime');
427
        //TODO fix check
428
        //TODO check if diff makes sense
429
        //TODO check upper limit
430 2
        return (int)(round($diff/self::TIME_SLOT_DURATION)+1); //TODO can round cause overlaps?
431
    }
432
433
    /**
434
     * @param int $timeslot
435
     * @return \DateTime
436
     */
437 2
    private function toDateTime($timeslot)
438
    {
439 2
        $ret = clone $this->startTime;
440 2
        return $ret->add(new \DateInterval('PT'.(($timeslot-1)*self::TIME_SLOT_DURATION).'S'));
441
    }
442
}