Completed
Push — master ( fe7ae6...ea593a )
by claudio
07:25
created

Optimise::toTimeSlot()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 13
ccs 6
cts 6
cp 1
rs 9.4286
cc 1
eloc 6
nc 1
nop 1
crap 1
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));
234
    }
235
236
    /**
237
     * @param mixed $item
238
     * @return mixed
239
     */
240 6
    private function durationConverter($item)
241
    {
242
        return $item->each(function ($item2) {
243 6
            $item2->duration = $this->convertDuration((int)$item2->duration);
244 6
            return $item2;
245
            //TODO try catch
246 6
        });
247
    }
248
249
    /**
250
     * @param int $duration
251
     * @return int
252
     */
253 6
    static private function convertDuration($duration)
254
    {
255 6
        return (int)ceil($duration / config('app.timeslots.duration'));
256
    }
257
258
    /**
259
     * @param mixed $item
260
     * @return mixed
261
     */
262 6
    private function timeSlotsConverter($item)
263
    {
264
        return $item->each(function ($item2) {
265 6
            $item2->time_start = $this->toTimeSlot($item2->time_start);
266 6
            $item2->time_end = $this->toTimeSlot($item2->time_end);
267 6
            return $item2;
268
            //TODO try catch
269 6
        });
270
    }
271
272
    /**
273
     * @param mixed $time
274
     * @return int
275
     * @throws OptimiseException
276
     */
277 6
    private function toTimeSlot($time)
278
    {
279 6
        $dateTime = new \DateTime($time);
280 6
        $diff = $dateTime->diff($this->startTime);
281 6
        $diff = explode(':', $diff->format('%R:%d:%h:%i:%s'));
282 6
        $diff = $diff[1] * 86400 + $diff[2] * 3600 + $diff[3] * 60 + $diff[4];
283
        //if($diff[0] != '-' && $diff != 0)
284
        //  throw new OptimiseException('timeslot time <= startTime');
285
        //TODO fix check
286
        //TODO check if diff makes sense
287
        //TODO check upper limit
288 6
        return (int)(round($diff / config('app.timeslots.duration')) + 1); //TODO can round cause overlaps?
289
    }
290
291
    /**
292
     * @param \Illuminate\Support\Collection $timeSlots
293
     * @param bool|true $free if true the array is filled with 1 for timeslots values else with 0 for timeslots values
294
     * @return array
295
     */
296 6
    static private function getAvailabilityArray(\Illuminate\Support\Collection $timeSlots, $timeslotsN, $free = true)
297
    {
298 6
        $ret = [];
299 6
        foreach ($timeSlots as $id => $timeSlots2) {
300 6
            $ret = self::fillTimeSlots($ret, $id, $timeSlots2, $free ? '1' : '0');
301 6
            $ret = self::fillRow($ret, $id, $timeslotsN, $free ? '0' : '1');
302 4
        }
303
304 6
        return $ret;
305
    }
306
307
    /**
308
     * @param array $array
309
     * @param int $id
310
     * @param \Illuminate\Support\Collection $timeSlots
311
     * @param string $fill
312
     * @return array
313
     */
314 6
    static private function fillTimeSlots(array $array, $id, \Illuminate\Support\Collection $timeSlots, $fill = '0')
315
    {
316 6
        foreach ($timeSlots as $timeSlot) {
317 6
            if (!isset($array[$id]))
318 6
                $array[$id] = [];
319 6
            $array[$id] = self::arrayPadInterval($array[$id], $timeSlot->time_start, $timeSlot->time_end, $fill);
320 4
        }
321 6
        return $array;
322
    }
323
324
    /**
325
     * @param array $array
326
     * @param int $from
327
     * @param int $to
328
     * @param string $pad
329
     * @return array
330
     */
331 6
    static private function arrayPadInterval(array $array, $from, $to, $pad = '0')
332
    {
333 6
        for ($i = $from; $i < $to; $i++)
334 6
            $array[$i] = $pad;
335 6
        return $array;
336
    }
337
338
    /**
339
     * @param array $array
340
     * @param int $id
341
     * @param string $fill
342
     * @return array
343
     */
344 6
    static private function fillRow(array $array, $id, $until, $fill = '0')
345
    {
346 6
        for ($i = 1; $i <= $until; $i++) {
347 6
            if (!isset($array[$id][$i]))
348 6
                $array[$id][$i] = $fill;
349 4
        }
350
351 6
        return $array;
352
    }
353
354
    /**
355
     * @param Solver $solver
356
     * @return Solver
357
     * @throws OptimiseException
358
     */
359 6
    private function setUserAvailability(Solver $solver)
360
    {
361
        /**
362
         * @var $users \Illuminate\Support\Collection
363
         */
364 6
        $users = collect($this->company->getEmployeesTimeSlots($this->startTime, $this->endTime));
365 6
        if($users->count() == 0)
366 4
            throw ((new OptimiseException("No users for this company"))->withEmpty(true));
367
        $timeslots = $users->groupBy('id')->map(function ($item) { //convert timeslots
368 6
            return $this->timeSlotsConverter($item);
369 6
        });
370 6
        return $solver->setUsersAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, false));
371
    }
372
373
    /**
374
     * @param Solver $solver
375
     * @return Solver
376
     * @throws OptimiseException
377
     */
378 6
    private function setUsersMeetings(Solver $solver)
379
    {
380 6
        $users = $solver->getUsers();
381 6
        $meetings = $solver->getMeetings();
382
        /**
383
         * @var $usersMeetings \Illuminate\Support\Collection
384
         */
385 6
        $usersMeetings = collect($this->company->getUsersMeetings($users, $meetings))->groupBy('employee_id');
386 6
        if($usersMeetings->count() == 0)
387 4
            throw ((new OptimiseException("No users for any meeting"))->withEmpty(true));
388
389 6
        return $solver->setUsersMeetings(self::getUsersMeetingsArray($users, $meetings, $usersMeetings));
390
    }
391
392
    /**
393
     * @param array $users
394
     * @param array $meetings
395
     * @param \Illuminate\Support\Collection $usersMeetings
396
     * @return array
397
     */
398 6
    static private function getUsersMeetingsArray($users, $meetings, \Illuminate\Support\Collection $usersMeetings)
399
    {
400 6
        $ret = [];
401 6
        foreach ($users as $user) {
402 6
            $usersMeetingsTmp = $usersMeetings->get($user);
403 6
            foreach ($meetings as $meeting) {
404 6
                if ($usersMeetingsTmp->contains('meeting_id', $meeting)) {
405 6
                    $ret[$user][$meeting] = 1;
406 4
                } else {
407 6
                    $ret[$user][$meeting] = 0;
408
                }
409 4
            }
410 4
        }
411
412 6
        return $ret;
413
    }
414
415
    /**
416
     * @return Optimise
417
     * @throws OptimiseException
418
     */
419 6
    public function save()
420
    {
421 6
        if (!($this->solver instanceof Solver)) {
422
            \Event::fire(new ErrorEvent($this->company, 'solver is not an instace of Solver'));
423
            throw new OptimiseException('solver is not an instance of Solver');
424
            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...
425
        }
426
        //TODO check results before save them
427
428
        try {
429 6
            $this->saveMeetings($this->solver);
430 6
            $this->saveEmployeesMeetings($this->solver);
431
            //TODO use the correct exceptions to avoid to share private data
432 4
        } catch (\Exception $e) {
433
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
434
            throw new OptimiseException('Optimising error', 0, $e);
435
            //TODO catch specif exception
436
        }
437
        //TODO Is this the correct place?
438 6
        \Event::fire(new OkEvent($this->company));
439 6
        return $this;
440
    }
441
442
    /**
443
     * @param Solver $solver
444
     */
445 6
    private function saveMeetings(Solver $solver)
446
    {
447 6
        $meetings = $solver->getYResults();
448 6
        foreach ($meetings as $id => $meeting) {
449 6
            $meetingO = \plunner\Meeting::findOrFail($id);//TODO catch error
450 6
            $meetingO->start_time = $this->toDateTime(array_search('1', $meeting));
451 6
            $meetingO->save();
452 4
        }
453 6
    }
454
455
    /**
456
     * @param int $timeslot
457
     * @return \DateTime
458
     */
459 6
    private function toDateTime($timeslot)
460
    {
461 6
        $ret = clone $this->startTime;
462 6
        return $ret->add(new \DateInterval('PT' . (($timeslot - 1) * config('app.timeslots.duration')) . 'S'));
463
    }
464
465
    /**
466
     * @param Solver $solver
467
     */
468 6
    private function saveEmployeesMeetings(Solver $solver)
469
    {
470 6
        $employeesMeetings = $solver->getXResults();
471 6
        foreach ($employeesMeetings as $eId => $employeeMeetings) {
472 6
            $employee = \plunner\Employee::findOrFail($eId);
473 6
            $employeeMeetings = collect($employeeMeetings);
474 6
            $employeeMeetings = $employeeMeetings->filter(function ($item) {
475 6
                return $item == 1;
476 6
            });
477 6
            $employee->meetings()->attach($employeeMeetings->keys()->toArray());
478 4
        }
479
    }
480
}