Completed
Push — master ( 18ebf0...848848 )
by claudio
08:43
created

Optimise::fillRow()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 4
Bugs 1 Features 0
Metric Value
c 4
b 1
f 0
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.6667
cc 3
eloc 5
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
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 9
    public function __construct(company $company, Schedule $schedule, \Illuminate\Contracts\Foundation\Application $laravel)
69
    {
70 9
        $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 9
        $this->schedule = $schedule;
72 9
        $this->laravel = $laravel;
73 9
        $this->max_time_slots = config('app.timeslots.max');
74 9
        $this->time_slots = config('app.timeslots.number');
75
76
77 9
        $this->setStartTime((new \DateTime())->modify('next monday'));
78 9
    }
79
80
81
    /**
82
     * @param \DateTime $startTime
83
     */
84 9
    public function setStartTime(\DateTime $startTime)
85
    {
86 9
        $this->startTime = clone $startTime;
87 9
        $this->endTime = clone $this->startTime;
88 9
        $this->endTime->add(new \DateInterval('PT' . (($this->max_time_slots + $this->time_slots) *
89 9
                config('app.timeslots.duration')) . 'S'));
90 9
    }
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 9
    public function optimise()
155
    {
156
        try {
157 9
            $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 9
            $solver = $this->setData($solver);
159 6
            $solver = $solver->solve();
160 6
            $this->solver = $solver;
161 7
        }catch(OptimiseException $e) {
162 7
            throw $e;
163
        }catch (\Exception $e) {
164
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
165
            throw new OptimiseException('Optimising error', 0, $e);
166
            //TODO catch specif exception
167
        }
168 6
        return $this;
169
    }
170
171
    /**
172
     * @param Solver $solver
173
     * @return Solver
174
     * @throws OptimiseException
175
     */
176 9
    private function setData(Solver $solver)
177
    {
178 9
        $solver = $this->setTimeSlotsSolver($solver);
179 9
        $solver = $this->setUsers($solver);
180 6
        $solver = $this->setAllMeetingsInfo($solver);
181 6
        $solver = $this->setUserAvailability($solver);
182 6
        $solver = $this->setUsersMeetings($solver);
183 6
        return $solver;
184
    }
185
186
    //TODO fix php doc with exceptions
187
188
    /**
189
     * @param Solver $solver
190
     * @return Solver
191
     * @throws OptimiseException
192
     */
193 9
    private function setTimeSlotsSolver(Solver $solver)
194
    {
195 9
        return $solver->setTimeSlots($this->time_slots)->setMaxTimeSlots($this->max_time_slots);
196
    }
197
198
    /**
199
     * @param Solver $solver
200
     * @return Solver
201
     * @throws OptimiseException
202
     */
203 9
    private function setUsers(Solver $solver)
204
    {
205
        //since we consider busy timeslots, we need to get all users
206 9
        $users = $this->company->employees->pluck('id')->toArray();
207 9
        if(count($users) == 0)
208 7
            throw ((new OptimiseException("No users for this company"))->withEmpty(true));
209 6
        return $solver->setUsers($users);
210
    }
211
212
    /**
213
     * @param Solver $solver
214
     * @return Solver
215
     * @throws OptimiseException
216
     */
217 6
    private function setAllMeetingsInfo(Solver $solver)
218
    {
219
        /**
220
         * @var $meetings \Illuminate\Support\Collection
221
         */
222 6
        $meetings = collect($this->company->getMeetingsTimeSlots($this->startTime, $this->endTime));
223 6
        if($meetings->count() == 0)
224 4
            throw ((new OptimiseException("No meetings for this week"))->withEmpty(true));
225
        $timeslots = $meetings->groupBy('id')->map(function ($item) { //convert timeslots
226 6
            return $this->durationConverter($this->timeSlotsConverter($item));
227 6
        });
228 6
        return $solver->setMeetings($timeslots->keys()->toArray())
229 6
            ->setMeetingsDuration($meetings->pluck('duration', 'id')->toArray())
230 6
            ->setMeetingsAvailability(self::getAvailabilityArray($timeslots, $this->time_slots));
231
    }
232
233
    /**
234
     * @param mixed $item
235
     * @return mixed
236
     */
237 6
    private function durationConverter($item)
238
    {
239
        return $item->each(function ($item2) {
240 6
            $item2->duration = $this->convertDuration((int)$item2->duration);
241 6
            return $item2;
242
            //TODO try catch
243 6
        });
244
    }
245
246
    /**
247
     * @param int $duration
248
     * @return int
249
     */
250 6
    static private function convertDuration($duration)
251
    {
252 6
        return (int)ceil($duration / config('app.timeslots.duration'));
253
    }
254
255
    /**
256
     * @param mixed $item
257
     * @return mixed
258
     */
259 6
    private function timeSlotsConverter($item)
260
    {
261
        return $item->each(function ($item2) {
262 6
            $item2->time_start = $this->toTimeSlot($item2->time_start);
263 6
            $item2->time_end = $this->toTimeSlot($item2->time_end);
264 6
            return $item2;
265
            //TODO try catch
266 6
        });
267
    }
268
269
    /**
270
     * @param mixed $time
271
     * @return int
272
     * @throws OptimiseException
273
     */
274 6
    private function toTimeSlot($time)
275
    {
276 6
        $dateTime = new \DateTime($time);
277 6
        $diff = $dateTime->diff($this->startTime);
278 6
        $diff = explode(':', $diff->format('%R:%d:%h:%i:%s'));
279 6
        $diff = $diff[1] * 86400 + $diff[2] * 3600 + $diff[3] * 60 + $diff[4];
280
        //if($diff[0] != '-' && $diff != 0)
281
        //  throw new OptimiseException('timeslot time <= startTime');
282
        //TODO fix check
283
        //TODO check if diff makes sense
284
        //TODO check upper limit
285 6
        return (int)(round($diff / config('app.timeslots.duration')) + 1); //TODO can round cause overlaps?
286
    }
287
288
    /**
289
     * @param \Illuminate\Support\Collection $timeSlots
290
     * @param bool|true $free if true the array is filled with 1 for timeslots values else with 0 for timeslots values
291
     * @return array
292
     */
293 6
    static private function getAvailabilityArray(\Illuminate\Support\Collection $timeSlots, $timeslotsN, $free = true)
294
    {
295 6
        $ret = [];
296 6
        foreach ($timeSlots as $id => $timeSlots2) {
297 6
            $ret = self::fillTimeSlots($ret, $id, $timeSlots2, $free ? '1' : '0');
298 6
            $ret = self::fillRow($ret, $id, $timeslotsN, $free ? '0' : '1');
299 4
        }
300
301 6
        return $ret;
302
    }
303
304
    /**
305
     * @param array $array
306
     * @param int $id
307
     * @param \Illuminate\Support\Collection $timeSlots
308
     * @param string $fill
309
     * @return array
310
     */
311 6
    static private function fillTimeSlots(array $array, $id, \Illuminate\Support\Collection $timeSlots, $fill = '0')
312
    {
313 6
        foreach ($timeSlots as $timeSlot) {
314 6
            if (!isset($array[$id]))
315 6
                $array[$id] = [];
316 6
            $array[$id] = self::arrayPadInterval($array[$id], $timeSlot->time_start, $timeSlot->time_end, $fill);
317 4
        }
318 6
        return $array;
319
    }
320
321
    /**
322
     * @param array $array
323
     * @param int $from
324
     * @param int $to
325
     * @param string $pad
326
     * @return array
327
     */
328 6
    static private function arrayPadInterval(array $array, $from, $to, $pad = '0')
329
    {
330 6
        for ($i = $from; $i < $to; $i++)
331 6
            $array[$i] = $pad;
332 6
        return $array;
333
    }
334
335
    /**
336
     * @param array $array
337
     * @param int $id
338
     * @param string $fill
339
     * @return array
340
     */
341 6
    static private function fillRow(array $array, $id, $until, $fill = '0')
342
    {
343 6
        for ($i = 1; $i <= $until; $i++) {
344 6
            if (!isset($array[$id][$i]))
345 6
                $array[$id][$i] = $fill;
346 4
        }
347
348 6
        return $array;
349
    }
350
351
    /**
352
     * @param Solver $solver
353
     * @return Solver
354
     * @throws OptimiseException
355
     */
356 6
    private function setUserAvailability(Solver $solver)
357
    {
358
        /**
359
         * @var $users \Illuminate\Support\Collection
360
         */
361 6
        $users = collect($this->company->getEmployeesTimeSlots($this->startTime, $this->endTime));
362 6
        if($users->count() == 0)
363 4
            throw ((new OptimiseException("No users for this company"))->withEmpty(true));
364
        $timeslots = $users->groupBy('id')->map(function ($item) { //convert timeslots
365 6
            return $this->timeSlotsConverter($item);
366 6
        });
367 6
        return $solver->setUsersAvailability(self::getAvailabilityArray($timeslots, $this->time_slots, false));
368
    }
369
370
    /**
371
     * @param Solver $solver
372
     * @return Solver
373
     * @throws OptimiseException
374
     */
375 6
    private function setUsersMeetings(Solver $solver)
376
    {
377 6
        $users = $solver->getUsers();
378 6
        $meetings = $solver->getMeetings();
379
        /**
380
         * @var $usersMeetings \Illuminate\Support\Collection
381
         */
382 6
        $usersMeetings = collect($this->company->getUsersMeetings($users, $meetings))->groupBy('employee_id');
383 6
        if($usersMeetings->count() == 0)
384 4
            throw ((new OptimiseException("No users for any meeting"))->withEmpty(true));
385
386 6
        return $solver->setUsersMeetings(self::getUsersMeetingsArray($users, $meetings, $usersMeetings));
387
    }
388
389
    /**
390
     * @param array $users
391
     * @param array $meetings
392
     * @param \Illuminate\Support\Collection $usersMeetings
393
     * @return array
394
     */
395 6
    static private function getUsersMeetingsArray($users, $meetings, \Illuminate\Support\Collection $usersMeetings)
396
    {
397 6
        $ret = [];
398 6
        foreach ($users as $user) {
399 6
            $usersMeetingsTmp = $usersMeetings->get($user);
400 6
            foreach ($meetings as $meeting) {
401 6
                if ($usersMeetingsTmp->contains('meeting_id', $meeting)) {
402 6
                    $ret[$user][$meeting] = 1;
403 4
                } else {
404 6
                    $ret[$user][$meeting] = 0;
405
                }
406 4
            }
407 4
        }
408
409 6
        return $ret;
410
    }
411
412
    /**
413
     * @return Optimise
414
     * @throws OptimiseException
415
     */
416 6
    public function save()
417
    {
418 6
        if (!($this->solver instanceof Solver)) {
419
            \Event::fire(new ErrorEvent($this->company, 'solver is not an instace of Solver'));
420
            throw new OptimiseException('solver is not an instance of Solver');
421
            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...
422
        }
423
        //TODO check results before save them
424
425
        try {
426 6
            $this->saveMeetings($this->solver);
427 6
            $this->saveEmployeesMeetings($this->solver);
428 4
        } catch (\Exception $e) {
429
            \Event::fire(new ErrorEvent($this->company, $e->getMessage()));
430
            throw new OptimiseException('Optimising error', 0, $e);
431
            //TODO catch specif exception
432
        }
433
        //TODO Is this the correct place?
434 6
        \Event::fire(new OkEvent($this->company));
435 6
        return $this;
436
    }
437
438
    /**
439
     * @param Solver $solver
440
     */
441 6
    private function saveMeetings(Solver $solver)
442
    {
443 6
        $meetings = $solver->getYResults();
444 6
        foreach ($meetings as $id => $meeting) {
445 6
            $meetingO = \plunner\Meeting::findOrFail($id);//TODO catch error
446 6
            $meetingO->start_time = $this->toDateTime(array_search('1', $meeting));
447 6
            $meetingO->save();
448 4
        }
449 6
    }
450
451
    /**
452
     * @param int $timeslot
453
     * @return \DateTime
454
     */
455 6
    private function toDateTime($timeslot)
456
    {
457 6
        $ret = clone $this->startTime;
458 6
        return $ret->add(new \DateInterval('PT' . (($timeslot - 1) * config('app.timeslots.duration')) . 'S'));
459
    }
460
461
    /**
462
     * @param Solver $solver
463
     */
464 6
    private function saveEmployeesMeetings(Solver $solver)
465
    {
466 6
        $employeesMeetings = $solver->getXResults();
467 6
        foreach ($employeesMeetings as $eId => $employeeMeetings) {
468 6
            $employee = \plunner\Employee::findOrFail($eId);
469 6
            $employeeMeetings = collect($employeeMeetings);
470 6
            $employeeMeetings = $employeeMeetings->filter(function ($item) {
471 6
                return $item == 1;
472 6
            });
473 6
            $employee->meetings()->attach($employeeMeetings->keys()->toArray());
474 4
        }
475
    }
476
}