Completed
Push — master ( cb49ac...f14b58 )
by claudio
13:10
created

Optimise::setCompany()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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