DashboardViewCollapseHelper::sortMentions()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
ccs 0
cts 5
cp 0
crap 6
1
<?php
2
3
namespace App\Dashboard;
4
5
use App\Entity\Teacher;
6
use App\Settings\DashboardSettings;
7
use App\Sorting\Sorter;
8
use App\Sorting\SubstitutionViewItemStrategy;
0 ignored issues
show
Bug introduced by
The type App\Sorting\SubstitutionViewItemStrategy was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use App\Utils\ArrayUtils;
10
use InvalidArgumentException;
11
12
/**
13
 * Applies an algorithm in order to reduce the amount of items of one lesson to one.
14
 */
15
class DashboardViewCollapseHelper {
16
17
    public function __construct(private DashboardSettings $settings)
18
    {
19
    }
20
21
    public function collapseView(DashboardView $view, ?Teacher $teacher) {
22
        foreach($view->getLessons() as $lesson) {
23
            $this->collapseLesson($lesson, $view, $teacher);
24
        }
25
26
        // Post-validation
27
        $this->validateTimetableSupervisionsAndExamSupervisions($view);
28
29
        // Sort mentions and exams
30
        $this->sortMentions($view);
31
        $this->sortExams($view);
32
    }
33
34
    private function sortMentions(DashboardView $view): void {
35
        $mentions = $view->getSubstitutionMentions();
36
        $view->clearSubstitutionMentions();
37
38
        usort($mentions, fn(SubstitutionViewItem $viewItemA, SubstitutionViewItem $viewItemB) => $viewItemA->getSubstitution()->getLessonStart() - $viewItemB->getSubstitution()->getLessonEnd());
39
40
        foreach($mentions as $mention) {
41
            $view->addSubstitutonMention($mention);
42
        }
43
    }
44
45
    private function sortExams(DashboardView $view): void {
46
        $exams = $view->getExams();
47
        $view->clearExams();
48
49
        usort($exams, fn(ExamViewItem $examA, ExamViewItem $examB) => $examA->getExam()->getLessonStart() - $examB->getExam()->getLessonStart());
50
51
        foreach($exams as $exam) {
52
            $view->addExam($exam);
53
        }
54
    }
55
56
    private function validateTimetableSupervisionsAndExamSupervisions(DashboardView $view) {
57
        $lessonNumbers = $view->getLessonNumbers();
58
59
        foreach($lessonNumbers as $lessonNumber) {
60
            $lesson = $view->getLesson($lessonNumber, true);
61
62
            if($lesson === null) {
63
                continue;
64
            }
65
66
            $lessonBefore = $view->getLesson($lessonNumber - 1, false);
67
            $lessonAfter = $view->getLesson($lessonNumber, false);
68
69
            if($lessonBefore === null || $lessonAfter === null) {
70
                continue;
71
            }
72
73
            // is supervision?
74
            $isSupervision = count(ArrayUtils::filterByType($lesson->getItems(), SupervisionViewItem::class)) > 0;
75
            $isExamSupervisionBefore = count(ArrayUtils::filterByType($lessonBefore->getItems(), ExamSupervisionViewItem::class)) > 0;
76
            $isExamSupervisionAfter = count(ArrayUtils::filterByType($lessonAfter->getItems(), ExamSupervisionViewItem::class)) > 0;
77
78
            if($isSupervision === true && $isExamSupervisionBefore === true && $isExamSupervisionAfter === true) {
79
                $lesson->setWarning();
80
            }
81
        }
82
    }
83
84
    private function collapseLesson(DashboardLesson $lesson, DashboardView $view, ?Teacher $teacher): void {
85
        // Merge supervisions
86
        $this->mergeExamSupervisions($lesson);
87
88
        // Add exams because they may not cause any troubles
89
        $this->addExamsToView($lesson, $view);
90
91
        // Store all items
92
        $originalItems = $lesson->getItems();
93
94
        // ... and all items as we will re-add them
95
        $lesson->clearItems();
96
97
        // STEP 1: TIMETABLE LESSONS
98
        /** @var TimetableLessonViewItem[] $timetableLessons */
99
        $timetableLessons = ArrayUtils::filterByType($originalItems, TimetableLessonViewItem::class);
100
        $timetableCount = count($timetableLessons);
101
        $freeTimetableLessons = ArrayUtils::filterByType($originalItems, FreeLessonView::class);
102
        $mergeTimetableLessons = false;
103
104
        if($timetableCount === 1 && $timetableLessons[0]->getLesson() !== null) {
105
            if(count($freeTimetableLessons) > 0) {
106
                $lesson->addItem($freeTimetableLessons[0]);
107
            } else {
108
                $lesson->addItem($timetableLessons[0]);
109
            }
110
        } else if($timetableCount > 1) {
111
            if($this->canMergeTimetableLessons($timetableLessons)) {
112
                $mergeTimetableLessons = true;
113
                $lesson->addItem($this->mergeTimetableLessons($timetableLessons));
114
            } else {
115
                $lesson->setWarning();
116
                $lesson->replaceItems($originalItems);
117
                return;
118
            }
119
        }
120
121
        // STEP 2: SUPERVISIONS
122
        /** @var SupervisionViewItem[] $supervisions */
123
        $supervisions = ArrayUtils::filterByType($originalItems, SupervisionViewItem::class);
124
        $supervisionCount = count($supervisions);
125
126
        if($supervisionCount === 1) {
127
            $lesson->addItem($supervisions[0]);
128
        } else if($supervisionCount > 1) {
129
            $lesson->setWarning();
130
            $lesson->replaceItems($originalItems);
131
            return;
132
        }
133
134
        // STEP 3: SUBSTITUTIONS
135
        /** @var SubstitutionViewItem[] $originalSubstitutions */
136
        $originalSubstitutions = ArrayUtils::filterByType($originalItems, SubstitutionViewItem::class);
137
138
        // Classify
139
        $substitutions = [ ];
140
        $substitutionMentions = [ ];
141
142
        foreach($originalSubstitutions as $substitution) {
143
            if($teacher !== null && $this->isMentionedInSubstitution($substitution, $teacher) === true) {
144
                $substitutionMentions[] = $substitution;
145
            }
146
147
            if ($teacher === null || $this->onlyMentionedInSubstitution($substitution, $teacher) === false) {
148
                $substitutions[] = $substitution;
149
            } else {
150
                $substitutionMentions[] = $substitution;
151
            }
152
        }
153
154
        // Further classication
155
        /** @var SubstitutionViewItem[] $additionalSubstitutions */
156
        $additionalSubstitutions = array_values(array_filter($substitutions, [ $this, 'isAdditionalSubstitution']));
157
        /** @var SubstitutionViewItem[] $removableSubstitutions */
158
        $removableSubstitutions = array_values(array_filter($substitutions, fn(SubstitutionViewItem $viewItem) => $this->isRemovableSubstitution($viewItem, $teacher)));
159
        /** @var SubstitutionViewItem[] $defaultSubstitutions */
160
        $defaultSubstitutions = array_values(array_filter($substitutions, fn(SubstitutionViewItem $viewItem) => $this->isDefault($viewItem, $teacher)));
161
162
        $defaultSubstitutionsCount = $this->countDefaultSubstitutions($defaultSubstitutions);
163
164
        if(count($removableSubstitutions) > 1 || $defaultSubstitutionsCount > 1) {
165
            $lesson->setWarning();
166
            $lesson->replaceItems($originalItems);
167
            return;
168
        }
169
170
        foreach($removableSubstitutions as $substitution) {
171
            if($mergeTimetableLessons === false) {
172
                $lesson->clearItems();
173
            }
174
            $lesson->addItem($substitution);
175
        }
176
177
        if($defaultSubstitutionsCount > 0) {
178
            if($mergeTimetableLessons === false) {
179
                $lesson->clearItems();
180
            }
181
            $mergedDefaultSubstitutions = $this->mergeSubstitutions($defaultSubstitutions);
182
183
            foreach ($mergedDefaultSubstitutions as $substitution) {
184
                $lesson->addItem($substitution);
185
            }
186
        }
187
188
        // Add Non-Replacing substitutions
189
        foreach($additionalSubstitutions as $substitution) {
190
            if($lesson->hasItem($substitution) === false) {
191
                $lesson->addItem($substitution);
192
            }
193
        }
194
195
        // STEP 4: EXAM SUPERVISION
196
        /** @var ExamSupervisionViewItem[] $examSupervisions */
197
        $examSupervisions = ArrayUtils::filterByType($originalItems, ExamSupervisionViewItem::class);
198
        $examSupervisionCount = count($examSupervisions);
199
200
        if($examSupervisionCount > 1) {
201
            $lesson->setWarning();
202
            $lesson->replaceItems($originalItems);
203
            return;
204
        } else if($examSupervisionCount === 1) {
205
            $collision = false;
206
207
            foreach($lesson->getItems() as $item) {
208
                if(!($item instanceof SubstitutionViewItem) || $this->isDefault($item, $teacher)) {
209
                    $collision = true;
210
                }
211
            }
212
213
            if($collision === true) {
214
                $lesson->setWarning();
215
                $lesson->replaceItems($originalItems);
216
                return;
217
            } else {
218
                $lesson->clearItems();
219
                $lesson->addItem($examSupervisions[0]);
220
            }
221
        }
222
223
        // EVERYTHING WORKED SO FAR: move certain items to view
224
        foreach($substitutionMentions as $mention) {
225
            $view->addSubstitutonMention($mention);
226
        }
227
228
        foreach(ArrayUtils::filterByType($originalItems, ExamViewItem::class) as $examItem) {
229
            $view->addExam($examItem);
230
        }
231
232
        // ADD FREE HOURS
233
        if(count($lesson->getItems()) === 0) {
234
            $lesson->addItem(new TimetableLessonViewItem(null, [ ], null));
235
        }
236
237
        // ADD ALL ITEMS THAT HAVE NOT BEEN TAKE CONCIDERATION
238
        $consideredTypes = [
239
            ExamViewItem::class,
240
            SubstitutionViewItem::class,
241
            SupervisionViewItem::class,
242
            TimetableLessonViewItem::class,
243
            ExamSupervisionViewItem::class,
244
            FreeLessonView::class
245
        ];
246
247
        foreach($originalItems as $originalItem) {
248
            if(!in_array($originalItem::class, $consideredTypes)) {
249
                $lesson->addItem($originalItem);
250
            }
251
        }
252
    }
253
254
    /**
255
     * @param TimetableLessonViewItem[] $lessonViews
256
     * @return boolean
257
     */
258
    private function canMergeTimetableLessons(array $lessonViews) {
259
        $rooms = [ ];
260
        $locations = [ ];
261
262
        foreach($lessonViews as $view) {
263
            $lesson = $view->getLesson();
264
265
            if($lesson->getRoom() !== null) {
266
                $rooms[] = $lesson->getRoom()->getId();
267
            }
268
269
            if($lesson->getLocation() !== null) {
270
                $locations[] = $lesson->getLocation();
271
            }
272
        }
273
274
        $distinctRooms = array_unique($rooms);
275
        $distinctLocations = array_unique($locations);
276
277
        if(count($distinctRooms) === 0 && count($distinctLocations) === 0) {
278
            return true;
279
        }
280
281
        if(count($distinctRooms) === 1 && count($distinctLocations) === 0) {
282
            return true;
283
        }
284
285
        if(count($distinctRooms) === 0 && count($distinctLocations) === 1) {
286
            return true;
287
        }
288
289
        return false;
290
    }
291
292
    /**
293
     * @param TimetableLessonViewItem[] $lessonViews
294
     * @return TimetableLessonViewItem
295
     */
296
    private function mergeTimetableLessons(array $lessonViews) {
297
        if(count($lessonViews) === 0) {
298
            throw new InvalidArgumentException('$lessonView must at least contain one element.');
299
        }
300
301
        $absentGroups = [ ];
302
303
        foreach($lessonViews as $lessonView) {
304
            $absentGroups = array_merge($lessonView->getAbsentStudentGroups());
305
        }
306
307
        $firstView = array_shift($lessonViews);
308
309
        $view = new TimetableLessonViewItem($firstView->getLesson(), $absentGroups, $firstView->getAbsenceLesson());
310
311
        foreach($lessonViews as $lessonView) {
312
            $view->addAdditionalLesson($lessonView->getLesson());
313
        }
314
315
        return $view;
316
    }
317
318
    private function addExamsToView(DashboardLesson $lesson, DashboardView $view) {
319
        $items = $lesson->getItems();
320
        $lesson->clearItems();
321
322
        foreach($items as $item) {
323
            if($item instanceof ExamViewItem) {
324
                $view->addExam($item);
325
            } else {
326
                $lesson->addItem($item);
327
            }
328
        }
329
    }
330
331
    private function mergeExamSupervisions(DashboardLesson $lesson): void {
332
        $items = $lesson->getItems();
333
        $supervisions = [ ];
334
335
        $lesson->clearItems();
336
337
        foreach($items as $item) {
338
            if($item instanceof ExamSupervisionViewItem) {
339
                $supervisions[] = $item;
340
            } else {
341
                $lesson->addItem($item);
342
            }
343
        }
344
345
        /** @var ExamSupervisionViewItem[] $merged */
346
        $merged = [ ];
347
348
        foreach($supervisions as $supervision) {
349
            $firstExam = $supervision->getFirst();
350
351
            if($firstExam === null) {
352
                continue;
353
            }
354
355
            $isMerged = false;
356
357
            foreach($merged as $mergedSupervisionItem) {
358
                $firstMergedExam = $mergedSupervisionItem->getFirst();
359
360
                if($firstMergedExam !== null && $firstExam->getRoom() === $firstMergedExam->getRoom()) {
361
                    $isMerged = true;
362
                    $mergedSupervisionItem->addExam($firstExam);
363
                }
364
            }
365
366
            if($isMerged === false) {
367
                $merged[] = $supervision;
368
            }
369
        }
370
371
        foreach($merged as $item) {
372
            $lesson->addItem($item);
373
        }
374
    }
375
376
    /**
377
     * @param SubstitutionViewItem[] $substitutions
378
     * @return SubstitutionViewItem[]
379
     */
380
    private function mergeSubstitutions(array $substitutions) {
381
        /** @var SubstitutionViewItem[] $merged */
382
        $merged = [ ];
383
384
        foreach($substitutions as $substitutionViewItem) {
385
            $substitution = $substitutionViewItem->getSubstitution();
386
            $isMerged = false;
387
388
            foreach($merged as $mergedViewItem) {
389
                $mergedSubstitution = $mergedViewItem->getSubstitution();
390
391
                if($substitution->getType() === $mergedSubstitution->getType()
392
                    && $substitution->getSubject() === $mergedSubstitution->getSubject()
393
                    && $substitution->getRemark() === $mergedSubstitution->getRemark()
394
                    && ArrayUtils::areEqual($substitution->getRooms(), $mergedSubstitution->getRooms())
395
                    && $substitution->getRoomName() === $mergedSubstitution->getRoomName()) {
396
397
                    // merge study groups
398
                    foreach($substitution->getStudyGroups() as $studyGroup) {
399
                        if($mergedSubstitution->getStudyGroups()->contains($studyGroup) === false) {
400
                            $mergedSubstitution->addStudyGroup($studyGroup);
401
                        }
402
                    }
403
404
                    foreach ($substitution->getReplacementStudyGroups() as $studyGroup) {
405
                        if($mergedSubstitution->getReplacementStudyGroups()->contains($studyGroup) === false) {
406
                            $mergedSubstitution->addReplacementStudyGroup($studyGroup);
407
                        }
408
                    }
409
410
                    // merge teachers
411
                    foreach($substitution->getTeachers() as $teacher) {
412
                        if($mergedSubstitution->getTeachers()->contains($teacher) === false) {
413
                            $mergedSubstitution->addTeacher($teacher);
414
                        }
415
                    }
416
                    foreach($substitution->getReplacementTeachers() as $teacher) {
417
                        if($mergedSubstitution->getReplacementTeachers()->contains($teacher) === false) {
418
                            $mergedSubstitution->addReplacementTeacher($teacher);
419
                        }
420
                    }
421
422
                    $isMerged = true;
423
                }
424
            }
425
426
            if($isMerged === false) {
427
                $clonedSubstitution = $substitution->clone(); // Somehow, clone $substitution does not work (when renameing clone() to __clone())
428
                $item = new SubstitutionViewItem($clonedSubstitution, false, $substitutionViewItem->getStudents(), $substitutionViewItem->getAbsentStudentGroups(), $substitutionViewItem->getTimetableLesson(), $substitutionViewItem->getAbsenceLesson());
429
                $merged[] = $item;
430
            }
431
        }
432
433
        return $merged;
434
    }
435
436
    /**
437
     * @param SubstitutionViewItem[] $defaultSubstitutions
438
     * @return int
439
     */
440
    private function countDefaultSubstitutions(array $defaultSubstitutions) {
441
        $count = count($defaultSubstitutions);
442
443
        for($i = 0; $i < count($defaultSubstitutions); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
444
            for($j = $i + 1; $j < count($defaultSubstitutions); $j++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
445
                $leftSubstitution = $defaultSubstitutions[$i]->getSubstitution();
446
                $rightSubstitution = $defaultSubstitutions[$j]->getSubstitution();
447
448
                // If subject and room are same: remove count by 1
449
                if($leftSubstitution->getSubject() === $rightSubstitution->getSubject() && ArrayUtils::areEqual($leftSubstitution->getRooms(), $rightSubstitution->getRooms())) {
450
                    $count--;
451
                }
452
            }
453
        }
454
455
        return $count;
456
    }
457
458
    private function isRemovableSubstitution(SubstitutionViewItem $viewItem, ?Teacher $teacher) {
459
        if($this->isAdditionalSubstitution($viewItem)) {
460
            return false;
461
        }
462
463
        if(in_array($viewItem->getSubstitution()->getType(), $this->settings->getRemovableSubstitutionTypes())) {
464
            return true;
465
        }
466
467
        $substitution = $viewItem->getSubstitution();
468
469
        if($teacher !== null) {
470
            return $substitution->getTeachers()->contains($teacher) && $substitution->getReplacementTeachers()->contains($teacher) === false;
471
        }
472
473
        return false;
474
    }
475
476
    private function isAdditionalSubstitution(SubstitutionViewItem $viewItem) {
477
        return in_array($viewItem->getSubstitution()->getType(), $this->settings->getAdditionalSubstitutionTypes());
478
    }
479
480
    private function onlyMentionedInSubstitution(SubstitutionViewItem $viewItem, Teacher $teacher): bool {
481
        $substitution = $viewItem->getSubstitution();
482
483
        foreach($substitution->getTeachers() as $substitutionTeacher) {
484
            if($substitutionTeacher->getId() === $teacher->getId()) {
485
                return false;
486
            }
487
        }
488
489
        foreach($substitution->getReplacementTeachers() as $substitutionTeacher) {
490
            if($substitutionTeacher->getId() === $teacher->getId()) {
491
                return false;
492
            }
493
        }
494
495
        return true;
496
    }
497
498
    private function isMentionedInSubstitution(SubstitutionViewItem $viewItem, Teacher $teacher): bool {
499
        return !empty($viewItem->getSubstitution()->getRemark()) && preg_match('~\W*' . $teacher->getAcronym() . '\W*~', $viewItem->getSubstitution()->getRemark());
500
    }
501
502
    private function isDefault(SubstitutionViewItem $viewItem, ?Teacher $teacher) {
503
        return $this->isRemovableSubstitution($viewItem, $teacher) === false && $this->isAdditionalSubstitution($viewItem) === false;
504
    }
505
}