Completed
Push — master ( 71feda...776f21 )
by claudio
06:53
created

Optimise::timeSlotsConverter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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