Completed
Pull Request — master (#28)
by claudio
07:55 queued 02:59
created

Optimise   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 417
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 90.97%

Importance

Changes 21
Bugs 4 Features 0
Metric Value
wmc 43
c 21
b 4
f 0
lcom 1
cbo 4
dl 0
loc 417
ccs 131
cts 144
cp 0.9097
rs 8.3158

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A setStartTime() 0 6 1
A getMaxTimeSlots() 0 4 1
A setMaxTimeSlots() 0 4 1
A getTimeSlots() 0 4 1
A setTimeSlots() 0 4 1
A getCompany() 0 4 1
A setCompany() 0 4 1
A getSolver() 0 4 1
A optimise() 0 15 2
A save() 0 20 3
A saveMeetings() 0 9 2
A saveEmployeesMeetings() 0 13 2
A setData() 0 9 1
A setTimeSlotsSolver() 0 4 1
A setUsers() 0 6 1
A setAllMeetingsInfo() 0 13 1
A setUserAvailability() 0 11 1
A setUsersMeetings() 0 11 1
A getUsersMeetingsArray() 0 17 4
A timeSlotsConverter() 0 9 1
A getAvailabilityArray() 0 11 4
A fillTimeSlots() 0 9 3
A fillRow() 0 9 3
A arrayPadInterval() 0 6 2
A toTimeSlot() 0 13 1
A toDateTime() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Optimise often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Optimise, and based on these observations, apply Extract Interface, too.

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