Completed
Pull Request — master (#28)
by claudio
09:02
created

Optimise::saveMeetings()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 9
ccs 8
cts 8
cp 1
rs 9.6667
cc 2
eloc 6
nc 2
nop 1
crap 2
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 max timeslots can be an environment var
25
    const TIME_SLOT_DURATION = 900; //seconds -> 15 minutes
26
    const DEFAULT_MAX_TIME_SLOTS = 20; //max duration of a meeting in term of timeslots //20
27
    const DEFAULT_TIME_SLOTS = 672;  //total amount of timeslots that must be optimised -> one week 4*24*7 = 672
28
29
    private $max_time_slots = self::DEFAULT_MAX_TIME_SLOTS;
30
    private $time_slots = self::DEFAULT_TIME_SLOTS;
31
32
    //TODO timezone
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 2
        $solver = $this->setTimeSlotsSolver($solver);
233 2
        $solver = $this->setUsers($solver);
234 2
        $solver = $this->setAllMeetingsInfo($solver);
235 2
        $solver = $this->setUserAvailability($solver);
236 2
        $solver = $this->setUsersMeetings($solver);
237 2
        return $solver;
238
    }
239
240
    /**
241
     * @param Solver $solver
242
     * @return Solver
243
     * @throws OptimiseException
244
     */
245 2
    private function setTimeSlotsSolver(Solver $solver)
246
    {
247 2
        return $solver->setTimeSlots($this->time_slots)->setMaxTimeSlots($this->max_time_slots);
248
    }
249
250
    /**
251
     * @param Solver $solver
252
     * @return Solver
253
     */
254 2
    private function setUsers(Solver $solver)
255
    {
256
        //since we consider busy timeslots, we need to get all users
257 2
        $users = $this->company->employees->pluck('id')->toArray();
258 2
        return $solver->setUsers($users);
259
    }
260
261
    /**
262
     * @param Solver $solver
263
     * @return Solver
264
     */
265 2
    private function setAllMeetingsInfo(Solver $solver)
266
    {
267
        /**
268
         * @var $meetings \Illuminate\Support\Collection
269
         */
270 2
        $meetings = collect($this->company->getMeetingsTimeSlots($this->startTime, $this->endTime));
271
        $timeslots = $meetings->groupBy('id')->map(function($item) { //convert timeslots
272 2
                return $this->durationConverter($this->timeSlotsConverter($item));
273 2
            });
274 2
        return $solver->setMeetings($timeslots->keys()->toArray())
275 2
            ->setMeetingsDuration($meetings->pluck('duration','id')->toArray())
276 2
            ->setMeetingsAvailability(self::getAvailabilityArray($timeslots, $this->time_slots));
277
    }
278
279
    /**
280
     * @param Solver $solver
281
     * @return Solver
282
     * @throws OptimiseException
283
     */
284 2
    private function setUserAvailability(Solver $solver)
285
    {
286
        /**
287
         * @var $users \Illuminate\Support\Collection
288
         */
289 2
        $users = collect($this->company->getEmployeesTimeSlots($this->startTime, $this->endTime));
290
        $timeslots = $users->groupBy('id')->map(function($item) { //convert timeslots
291 2
                return $this->timeSlotsConverter($item);
292 2
            });
293 2
        return $solver->setUsersAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, false));
294
    }
295
296
    /**
297
     * @param Solver $solver
298
     * @return Solver
299
     * @throws OptimiseException
300
     */
301 2
    private function setUsersMeetings(Solver $solver)
302
    {
303 2
        $users = $solver->getUsers();
304 2
        $meetings = $solver->getMeetings();
305
        /**
306
         * @var $usersMeetings \Illuminate\Support\Collection
307
         */
308 2
        $usersMeetings = collect($this->company->getUsersMeetings($users, $meetings))->groupBy('employee_id');
309
310 2
        return $solver->setUsersMeetings(self::getUsersMeetingsArray($users, $meetings, $usersMeetings));
311
    }
312
313
    /**
314
     * @param array $users
315
     * @param array $meetings
316
     * @param \Illuminate\Support\Collection $usersMeetings
317
     * @return array
318
     */
319 2
    static private function getUsersMeetingsArray($users, $meetings, \Illuminate\Support\Collection $usersMeetings)
320
    {
321 2
        $ret = [];
322 2
        foreach($users as $user)
323
        {
324 2
            $usersMeetingsTmp = $usersMeetings->get($user);
325 2
            foreach($meetings as $meeting){
326 2
                if($usersMeetingsTmp->contains('meeting_id', $meeting)){
327 2
                    $ret[$user][$meeting] = 1;
328 2
                }else{
329 2
                    $ret[$user][$meeting] = 0;
330
                }
331 2
            }
332 2
        }
333
334 2
        return $ret;
335
    }
336
337
    /**
338
     * @param mixed $item
339
     * @return mixed
340
     */
341 2
    private function durationConverter($item)
342
    {
343
        return $item->each(function($item2){
344 2
            $item2->duration = $this->convertDuration((int) $item2->duration);
345 2
            return $item2;
346
            //TODO try catch
347 2
        });
348
    }
349
350
    /**
351
     * @param mixed $item
352
     * @return mixed
353
     */
354
    private function timeSlotsConverter($item)
355
    {
356 2
        return $item->each(function($item2){
357 2
            $item2->time_start = $this->toTimeSlot($item2->time_start);
358 2
            $item2->time_end = $this->toTimeSlot($item2->time_end);
359 2
            return $item2;
360
            //TODO try catch
361 2
        });
362
    }
363
364
    /**
365
     * @param \Illuminate\Support\Collection $timeSlots
366
     * @param bool|true $free if true the array is filled with 1 for timeslots values else with 0 for timeslots values
367
     * @return array
368
     */
369 2
    static private function getAvailabilityArray(\Illuminate\Support\Collection $timeSlots, $timeslotsN, $free=true)
370
    {
371 2
        $ret = [];
372 2
        foreach($timeSlots as $id=>$timeSlots2)
373
        {
374 2
            $ret = self::fillTimeSlots($ret, $id, $timeSlots2, $free?'1':'0');
375 2
            $ret = self::fillRow($ret, $id, $timeslotsN, $free?'0':'1');
376 2
        }
377
378 2
        return $ret;
379
    }
380
381
    /**
382
     * @param array $array
383
     * @param int $id
384
     * @param \Illuminate\Support\Collection $timeSlots
385
     * @param string $fill
386
     * @return array
387
     */
388 2
    static private function fillTimeSlots(array $array, $id, \Illuminate\Support\Collection $timeSlots, $fill = '0')
389
    {
390 2
        foreach($timeSlots as $timeSlot) {
391 2
            if(!isset($array[$id]))
392 2
                $array[$id] = [];
393 2
            $array[$id] = self::arrayPadInterval($array[$id], $timeSlot->time_start, $timeSlot->time_end, $fill);
394 2
        }
395 2
        return $array;
396
    }
397
398
    /**
399
     * @param array $array
400
     * @param int $id
401
     * @param string $fill
402
     * @return array
403
     */
404 2
    static private function fillRow(array $array, $id, $until, $fill = '0')
405
    {
406 2
        for($i = 1; $i <= $until; $i++){
407 2
            if(!isset($array[$id][$i]))
408 2
                $array[$id][$i] = $fill;
409 2
        }
410
411 2
        return $array;
412
    }
413
414
    /**
415
     * @param array $array
416
     * @param int $from
417
     * @param int $to
418
     * @param string $pad
419
     * @return array
420
     */
421 2
    static private function arrayPadInterval(array $array, $from, $to, $pad = '0')
422
    {
423 2
        for($i = $from; $i<$to; $i++)
424 2
            $array[$i] = $pad;
425 2
        return $array;
426
    }
427
428
429
    /**
430
     * @param mixed $time
431
     * @return int
432
     * @throws OptimiseException
433
     */
434 2
    private function toTimeSlot($time)
435
    {
436 2
        $dateTime = new \DateTime($time);
437 2
        $diff = $dateTime->diff($this->startTime);
438 2
        $diff = explode(':',$diff->format('%R:%d:%h:%i:%s'));
439 2
        $diff = $diff[1]*86400 + $diff[2]*3600 + $diff[3]*60 + $diff[4];
440
        //if($diff[0] != '-' && $diff != 0)
441
          //  throw new OptimiseException('timeslot time <= startTime');
442
        //TODO fix check
443
        //TODO check if diff makes sense
444
        //TODO check upper limit
445 2
        return (int)(round($diff/self::TIME_SLOT_DURATION)+1); //TODO can round cause overlaps?
446
    }
447
448
    /**
449
     * @param int $timeslot
450
     * @return \DateTime
451
     */
452 2
    private function toDateTime($timeslot)
453
    {
454 2
        $ret = clone $this->startTime;
455 2
        return $ret->add(new \DateInterval('PT'.(($timeslot-1)*self::TIME_SLOT_DURATION).'S'));
456
    }
457
458
    /**
459
     * @param int $duration
460
     * @return int
461
     */
462 2
    static private function convertDuration($duration)
463
    {
464 2
        return (int)ceil($duration/self::TIME_SLOT_DURATION);
465
    }
466
}