Completed
Push — master ( e5acfe...6f9b0c )
by claudio
07:02
created

Optimise::getUsersMeetingsArray()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 16
ccs 12
cts 12
cp 1
rs 8.8571
cc 5
eloc 10
nc 4
nop 3
crap 5
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
use plunner\Events\Optimise\OkEvent;
15
16
/**
17
 * Class Optimise
18
 * @author Claudio Cardinale <[email protected]>
19
 * @copyright 2015 Claudio Cardinale
20
 * @version 1.0.0
21
 * @package plunner\Console\Commands\Optimise
22
 */
23
class Optimise
24
{
25
26
    private $max_time_slots;
27
    private $time_slots;
28
29
    //TODO timezone
30
    /**
31
     * @var \DateTime
32
     */
33
    private $startTime;
34
    /**
35
     * @var \DateTime
36
     */
37
    private $endTime;
38
39
    /**
40
     * @var Company
41
     */
42
    private $company;
43
44
    /**
45
     * @var Schedule laravel schedule object needed to perform command in background
46
     */
47
    private $schedule;
48
49
    /**
50
     * @var \Illuminate\Contracts\Foundation\Application;
51
     */
52
    private $laravel;
53
54
    /**
55
     * @var Solver
56
     */
57
    private $solver = null;
58
59
    //TODO clone
60
    //TODO to_string
61
62
    /**
63
     * Optimise constructor.
64
     * @param company $company
65
     * @param Schedule $schedule
66
     * @param \Illuminate\Contracts\Foundation\Application $laravel
67
     */
68 12
    public function __construct(company $company, Schedule $schedule, \Illuminate\Contracts\Foundation\Application $laravel)
69
    {
70 12
        $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...
71 12
        $this->schedule = $schedule;
72 12
        $this->laravel = $laravel;
73 12
        $this->max_time_slots = config('app.timeslots.max');
74 12
        $this->time_slots = config('app.timeslots.number');
75
76
77 12
        $this->setStartTime((new \DateTime())->modify('next monday'));
78 12
    }
79
80
81
    /**
82
     * @param \DateTime $startTime
83
     */
84 12
    public function setStartTime(\DateTime $startTime)
85
    {
86 12
        $this->startTime = clone $startTime;
87 12
        $this->endTime = clone $this->startTime;
88 12
        $this->endTime->add(new \DateInterval('PT' . (($this->max_time_slots + $this->time_slots) *
89 12
                config('app.timeslots.duration')) . 'S'));
90 12
    }
91
92
    /**
93
     * @return int
94
     */
95 2
    public function getMaxTimeSlots()
96
    {
97 2
        return $this->max_time_slots;
98
    }
99
100
    /**
101
     * @param int $max_time_slots
102
     */
103 3
    public function setMaxTimeSlots($max_time_slots)
104
    {
105 3
        $this->max_time_slots = $max_time_slots;
106 3
    }
107
108
    /**
109
     * @return int
110
     */
111 3
    public function getTimeSlots()
112
    {
113 3
        return $this->time_slots;
114
    }
115
116
    /**
117
     * @param int $time_slots
118
     */
119 3
    public function setTimeSlots($time_slots)
120
    {
121 3
        $this->time_slots = $time_slots;
122 3
    }
123
124
125
    /**
126
     * @return Company
127
     */
128
    public function getCompany()
129
    {
130
        return $this->company;
131
    }
132
133
    /**
134
     * @param Company $company
135
     */
136
    public function setCompany($company)
137
    {
138
        $this->company = $company;
139
    }
140
141
    /**
142
     * @return Solver
143
     */
144 5
    public function getSolver()
145 4
    {
146 3
        return $this->solver;
147
    }
148
149
150
    /**
151
     * @return Optimise
152
     * @throws OptimiseException
153
     */
154 12
    public function optimise()
155
    {
156
        try {
157 12
            $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...
158 12
            $solver = $this->setData($solver);
159 6
            $solver = $solver->solve();
160 6
            $this->solver = $solver;
161 10
        }catch(OptimiseException $e) {
162 10
            if(!$e->isEmpty())
163 4
                \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
164 6
            throw $e;
165
        }catch (\Exception $e) {
166
            //TODO use the correct exceptions to avoid to share private data
167
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
168
            throw new OptimiseException('Optimising error', 0, $e);
169
            //TODO catch specif exception
170
        }
171 6
        return $this;
172
    }
173
174
    /**
175
     * @param Solver $solver
176
     * @return Solver
177
     * @throws OptimiseException
178
     */
179 12
    private function setData(Solver $solver)
180
    {
181 12
        $solver = $this->setTimeSlotsSolver($solver);
182 12
        $solver = $this->setUsers($solver);
183 9
        $solver = $this->setAllMeetingsInfo($solver);
184 6
        $solver = $this->setUserAvailability($solver);
185 6
        $solver = $this->setUsersMeetings($solver);
186 6
        return $solver;
187
    }
188
189
    //TODO fix php doc with exceptions
190
191
    /**
192
     * @param Solver $solver
193
     * @return Solver
194
     * @throws OptimiseException
195
     */
196 12
    private function setTimeSlotsSolver(Solver $solver)
197
    {
198 12
        return $solver->setTimeSlots($this->time_slots)->setMaxTimeSlots($this->max_time_slots);
199
    }
200
201
    /**
202
     * @param Solver $solver
203
     * @return Solver
204
     * @throws OptimiseException
205
     */
206 12
    private function setUsers(Solver $solver)
207
    {
208
        //since we consider busy timeslots, we need to get all users
209 12
        $users = $this->company->employees->pluck('id')->toArray();
210 12
        if(count($users) == 0)
211 9
            throw ((new OptimiseException("No users for this company"))->withEmpty(true));
212 9
        return $solver->setUsers($users);
213
    }
214
215
    /**
216
     * @param Solver $solver
217
     * @return Solver
218
     * @throws OptimiseException
219
     */
220 9
    private function setAllMeetingsInfo(Solver $solver)
221
    {
222
        /**
223
         * @var $meetings \Illuminate\Support\Collection
224
         */
225 9
        $meetings = collect($this->company->getMeetingsTimeSlots($this->startTime, $this->endTime));
226 9
        if($meetings->count() == 0)
227 7
            throw ((new OptimiseException("No meetings for this week"))->withEmpty(true));
228
        $timeslots = $meetings->groupBy('id')->map(function ($item) { //convert timeslots
229 6
            return $this->durationConverter($this->timeSlotsConverter($item));
230 6
        });
231 6
        return $solver->setMeetings($timeslots->keys()->toArray())
232 6
            ->setMeetingsDuration($meetings->pluck('duration', 'id')->toArray())
233 6
            ->setMeetingsAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, $solver->getMeetings()));
234
    }
235
236
237
    /**
238
     * @param mixed $item
239
     * @return mixed
240
     */
241 6
    private function durationConverter($item)
242
    {
243
        return $item->each(function ($item2) {
244 6
            $item2->duration = $this->convertDuration((int)$item2->duration);
245 6
            return $item2;
246
            //TODO try catch
247 6
        });
248
    }
249
250
    /**
251
     * @param int $duration
252
     * @return int
253
     */
254 6
    static private function convertDuration($duration)
255
    {
256 6
        return (int)ceil($duration / config('app.timeslots.duration'));
257
    }
258
259
    /**
260
     * @param mixed $item
261
     * @return mixed
262
     */
263 6
    private function timeSlotsConverter($item)
264
    {
265
        return $item->each(function ($item2) {
266 6
            $item2->time_start = $this->toTimeSlot($item2->time_start);
267 6
            $item2->time_end = $this->toTimeSlot($item2->time_end);
268 6
            return $item2;
269
            //TODO try catch
270 6
        });
271
    }
272
273
    /**
274
     * @param mixed $time
275
     * @return int
276
     * @throws OptimiseException
277
     */
278 6
    private function toTimeSlot($time)
279
    {
280 6
        $dateTime = new \DateTime($time);
281 6
        $diff = $dateTime->diff($this->startTime);
282 6
        $diff = explode(':', $diff->format('%R:%d:%h:%i:%s'));
283 6
        $diff = $diff[1] * 86400 + $diff[2] * 3600 + $diff[3] * 60 + $diff[4];
284
        //if($diff[0] != '-' && $diff != 0)
285
        //  throw new OptimiseException('timeslot time <= startTime');
286
        //TODO fix check
287
        //TODO check if diff makes sense
288
        //TODO check upper limit
289 6
        return (int)(round($diff / config('app.timeslots.duration')) + 1); //TODO can round cause overlaps?
290
    }
291
292
    /**
293
     * @param \Illuminate\Support\Collection $timeSlots
294
     * @param bool|true $free if true the array is filled with 1 for timeslots values else with 0 for timeslots values
295
     * @return array
296
     */
297 6
    static private function getAvailabilityArray(\Illuminate\Support\Collection $timeSlots, $timeslotsN, $ids, $free = true)
298
    {
299 6
        $ret = [];
300 6
        foreach ($ids as $id) {
301 6
            if(isset($timeSlots[$id]))
302 6
                $ret = self::fillTimeSlots($ret, $id, $timeSlots[$id], $free ? '1' : '0');
303 6
            $ret = self::fillRow($ret, $id, $timeslotsN, $free ? '0' : '1');
304 4
        }
305
306 6
        return $ret;
307
    }
308
309
    /**
310
     * @param array $array
311
     * @param int $id
312
     * @param \Illuminate\Support\Collection $timeSlots
313
     * @param string $fill
314
     * @return array
315
     */
316 6
    static private function fillTimeSlots(array $array, $id, \Illuminate\Support\Collection $timeSlots, $fill = '0')
317
    {
318 6
        foreach ($timeSlots as $timeSlot) {
319 6
            if (!isset($array[$id]))
320 6
                $array[$id] = [];
321 6
            $array[$id] = self::arrayPadInterval($array[$id], $timeSlot->time_start, $timeSlot->time_end, $fill);
322 4
        }
323 6
        return $array;
324
    }
325
326
    /**
327
     * @param array $array
328
     * @param int $from
329
     * @param int $to
330
     * @param string $pad
331
     * @return array
332
     */
333 6
    static private function arrayPadInterval(array $array, $from, $to, $pad = '0')
334
    {
335 6
        for ($i = $from; $i < $to; $i++)
336 6
            $array[$i] = $pad;
337 6
        return $array;
338
    }
339
340
    /**
341
     * @param array $array
342
     * @param int $id
343
     * @param string $fill
344
     * @return array
345
     */
346 6
    static private function fillRow(array $array, $id, $until, $fill = '0')
347
    {
348 6
        for ($i = 1; $i <= $until; $i++) {
349 6
            if (!isset($array[$id][$i]))
350 6
                $array[$id][$i] = $fill;
351 4
        }
352
353 6
        return $array;
354
    }
355
356
    /**
357
     * @param Solver $solver
358
     * @return Solver
359
     * @throws OptimiseException
360
     */
361 6
    private function setUserAvailability(Solver $solver)
362
    {
363
        /**
364
         * @var $users \Illuminate\Support\Collection
365
         */
366 6
        $users = collect($this->company->getEmployeesTimeSlots($this->startTime, $this->endTime));
367
        //if($users->count() == 0)
368
        //    throw ((new OptimiseException("No users for this company"))->withEmpty(true));
369
        $timeslots = $users->groupBy('id')->map(function ($item) { //convert timeslots
370 6
            return $this->timeSlotsConverter($item);
371 6
        });
372 6
        return $solver->setUsersAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, $solver->getUsers(),
373 6
            false));
374
    }
375
376
    /**
377
     * @param Solver $solver
378
     * @return Solver
379
     * @throws OptimiseException
380
     */
381 6
    private function setUsersMeetings(Solver $solver)
382
    {
383 6
        $users = $solver->getUsers();
384 6
        $meetings = $solver->getMeetings();
385
        /**
386
         * @var $usersMeetings \Illuminate\Support\Collection
387
         */
388 6
        $usersMeetings = collect($this->company->getUsersMeetings($users, $meetings))->groupBy('employee_id');
389 6
        if($usersMeetings->count() == 0)
390 4
            throw ((new OptimiseException("No users for any meeting"))->withEmpty(true));
391
392 6
        return $solver->setUsersMeetings(self::getUsersMeetingsArray($users, $meetings, $usersMeetings));
393
    }
394
395
    /**
396
     * @param array $users
397
     * @param array $meetings
398
     * @param \Illuminate\Support\Collection $usersMeetings
399
     * @return array
400
     */
401 6
    static private function getUsersMeetingsArray($users, $meetings, \Illuminate\Support\Collection $usersMeetings)
402
    {
403 6
        $ret = [];
404 6
        foreach ($users as $user) {
405 6
            $usersMeetingsTmp = $usersMeetings->get($user);
406 6
            foreach ($meetings as $meeting) {
407 6
                if ($usersMeetingsTmp != null && $usersMeetingsTmp->contains('meeting_id', $meeting)) {
408 6
                    $ret[$user][$meeting] = 1;
409 4
                } else {
410 6
                    $ret[$user][$meeting] = 0;
411
                }
412 4
            }
413 4
        }
414
415 6
        return $ret;
416
    }
417
418
    /**
419
     * @return Optimise
420
     * @throws OptimiseException
421
     */
422 6
    public function save()
423
    {
424 6
        if (!($this->solver instanceof Solver)) {
425
            \Event::fire(new ErrorEvent($this->company, 'solver is not an instace of Solver'));
426
            throw new OptimiseException('solver is not an instance of Solver');
427
            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...
428
        }
429
        //TODO check results before save them
430
431
        try {
432 6
            $this->saveMeetings($this->solver);
433 6
            $this->saveEmployeesMeetings($this->solver);
434
            //TODO use the correct exceptions to avoid to share private data
435 4
        } catch (\Exception $e) {
436
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
437
            throw new OptimiseException('Optimising error', 0, $e);
438
            //TODO catch specif exception
439
        }
440
        //TODO Is this the correct place?
441 6
        \Event::fire(new OkEvent($this->company));
442 6
        return $this;
443
    }
444
445
    /**
446
     * @param Solver $solver
447
     */
448 6
    private function saveMeetings(Solver $solver)
449
    {
450 6
        $meetings = $solver->getYResults();
451 6
        foreach ($meetings as $id => $meeting) {
452 6
            $meetingO = \plunner\Meeting::findOrFail($id);//TODO catch error
453 6
            $meetingO->start_time = $this->toDateTime(array_search('1', $meeting));
454 6
            $meetingO->save();
455 4
        }
456 6
    }
457
458
    /**
459
     * @param int $timeslot
460
     * @return \DateTime
461
     */
462 6
    private function toDateTime($timeslot)
463
    {
464 6
        $ret = clone $this->startTime;
465 6
        return $ret->add(new \DateInterval('PT' . (($timeslot - 1) * config('app.timeslots.duration')) . 'S'));
466
    }
467
468
    /**
469
     * @param Solver $solver
470
     */
471 6
    private function saveEmployeesMeetings(Solver $solver)
472
    {
473 6
        $employeesMeetings = $solver->getXResults();
474 6
        foreach ($employeesMeetings as $eId => $employeeMeetings) {
475 6
            $employee = \plunner\Employee::findOrFail($eId);
476 6
            $employeeMeetings = collect($employeeMeetings);
477 6
            $employeeMeetings = $employeeMeetings->filter(function ($item) {
478 6
                return $item == 1;
479 6
            });
480 6
            $employee->meetings()->attach($employeeMeetings->keys()->toArray());
481 4
        }
482
    }
483
}