Issues (964)

src/Timetable/TimetableHelper.php (3 issues)

1
<?php
2
3
namespace App\Timetable;
4
5
use App\Date\WeekOfYear;
6
use App\Entity\TimetableLesson as TimetableLessonEntity;
7
use App\Entity\TimetableSupervision;
8
use App\Repository\AppointmentCategoryRepositoryInterface;
9
use App\Repository\AppointmentRepositoryInterface;
10
use App\Repository\FreeTimespanRepositoryInterface;
11
use App\Repository\TimetableWeekRepositoryInterface;
12
use App\Settings\TimetableSettings;
13
use App\Utils\ArrayUtils;
14
use DateTime;
15
use SchulIT\CommonBundle\Helper\DateHelper;
16
17
/**
18
 * Helper which transforms a list of TimetableLessons
19
 * into a Timetable object for easy traversing
20
 */
21
class TimetableHelper {
22
23
    public function __construct(private DateHelper $dateHelper, private TimetableSettings $settings, private AppointmentRepositoryInterface $appointmentRepository, private FreeTimespanRepositoryInterface $freeTimespanRepository, private AppointmentCategoryRepositoryInterface $appointmentCategoryRepository, private TimetableWeekRepositoryInterface $weekRepository)
24
    {
25
    }
26
27
    /**
28
     * @param WeekOfYear[] $weeks
29
     * @param TimetableLessonEntity[] $lessons
30
     * @param TimetableSupervision[] $supervision
31
     */
32
    public function makeTimetable(array $weeks, array $lessons, array $supervision = [ ]): Timetable {
33
        $timetable = new Timetable();
34
35
        $freeDays = $this->getFreeDays();
36
37
        foreach($weeks as $week) {
38
            $timetable->addWeek(
39
                $this->makeTimetableWeek($week, $lessons, $supervision, $freeDays)
40
            );
41
        }
42
43
        $this->addEmptyLessons($timetable);
44
        $this->collapseTimetable($timetable);
45
        $this->ensureAllLessonsAreDisplayed($timetable);
46
47
        return $timetable;
48
    }
49
50
    /**
51
     * @return DateTime[]
52
     */
53
    private function getFreeDays(): array {
54
        $categoryIds = $this->settings->getCategoryIds();
55
        $freeCategories = [ ];
56
57
        foreach($this->appointmentCategoryRepository->findAll() as $category) {
58
            if(in_array($category->getId(), $categoryIds)) {
59
                $freeCategories[] = $category;
60
            }
61
        }
62
63
        if(count($freeCategories) === 0) {
64
            // In case there are no free categories, return fast (findAll([]) will not filter for any category!)
65
            return [];
66
        }
67
68
        $appointments = $this->appointmentRepository->findAll($freeCategories);
69
70
        $freeDays = [ ];
71
72
        foreach($appointments as $appointment) {
73
            if($appointment->isAllDay() === false) {
74
                continue;
75
            }
76
77
            $date = clone $appointment->getStart();
78
            while($date < $appointment->getEnd()) {
79
                $freeDays[] = $date;
80
81
                $date = (clone $date)->modify('+1 day');
82
            }
83
        }
84
85
        foreach($this->freeTimespanRepository->findAll() as $timespan) {
86
            if($timespan->getStart() === 1 && $timespan->getEnd() === $this->settings->getMaxLessons()) {
87
                $freeDays[] = $timespan->getDate();
88
            }
89
        }
90
91
        return $freeDays;
92
    }
93
94
    /**
95
     * Ensures that no lessons are missed out even if they are free because otherwise
96
     * rendering will glitch.
97
     */
98
    private function ensureAllLessonsAreDisplayed(Timetable $timetable): void {
99
        $numberOfLessons = $this->settings->getMaxLessons();
100
101
        foreach($timetable->getWeeks() as $week) {
102
            $week->setMaxLesson($numberOfLessons);
103
        }
104
    }
105
106
    /**
107
     * Adds empty TimetableLessons in order to improve collapsing capabilitites
108
     */
109
    private function addEmptyLessons(Timetable $timetable) {
110
        foreach($timetable->getWeeks() as $week) {
111
            $maxLessons = $week->getMaxLessons();
112
113
            foreach($week->days as $day) {
114
                $lessons = $day->getLessonsContainers();
115
116
                for($lesson = 1; $lesson <= $maxLessons; $lesson++) {
117
                    if(array_key_exists($lesson, $lessons) !== true) {
118
                        $day->addEmptyTimetableLessonsContainer($lesson);
119
                    }
120
                }
121
            }
122
        }
123
    }
124
125
    /**
126
     * Computes the model for double lessons such that the model knows which lessons are collapsed. (Does NOT compute
127
     * which lessons are considered double lessons -> this information must be set at import)
128
     */
129
    private function collapseTimetable(Timetable $timetable) {
130
        foreach($timetable->getWeeks() as $week) {
131
            foreach($week->days as $day) {
132
                for($lessonNumber = 1; $lessonNumber <= count($day->getLessonsContainers()); $lessonNumber++) {
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...
133
                    $container = $day->getTimetableLessonsContainer($lessonNumber);
134
135
                    if(count($container->getSupervisions()) > 0) {
136
                        continue; // do not collapse if contains supervisions
137
                    }
138
139
                    if(count($container->getLessons()) === 0) {
140
                        continue; // no lessons -> continue (important so we can use $durations[0] afterwards)
141
                    }
142
143
                    $durations = [ ];
144
                    foreach($container->getLessons() as $lesson) {
145
                        $durations[] = $lesson->getLessonEnd() - $lesson->getLessonStart() + 1;
146
                    }
147
148
                    if(min($durations) !== max($durations) && $durations[0] > 1) { // dirty condition for "not all numbers are same"
149
                        continue;
150
                    }
151
152
                    $duration = $durations[0];
153
154
                    // now check if following lessons have same lessons (they might have additional lessons) or have
155
                    // supervisions before them (also, then do not collapse)
156
157
                    $lessonIds = array_map(fn(TimetableLessonEntity $lesson) => $lesson->getId(), $container->getLessons());
158
159
                    for($nextLessonNumber = $lessonNumber + 1; $nextLessonNumber <= $lessonNumber + $duration - 1; $nextLessonNumber++) {
160
                        $nextLessonContainer = $day->getTimetableLessonsContainer($nextLessonNumber);
161
162
                        if($nextLessonContainer->hasSupervisionBefore()) {
163
                            continue 2; // continue to outer loop as collapsing is not possible
164
                        }
165
166
                        // Now check if lessons are same
167
                        $nextLessonIds = array_map(fn(TimetableLessonEntity $lesson) => $lesson->getId(), $nextLessonContainer->getLessons());
168
169
                        if(ArrayUtils::areEqual($lessonIds, $nextLessonIds)) {
170
                            $container->setRowSpan($container->getRowSpan() + 1);
171
                            $nextLessonContainer->clear();
172
                            $nextLessonContainer->setRowSpan(0);
173
                        }
174
                    }
175
                }
176
            }
177
        }
178
    }
179
180
    /**
181
     * @param TimetableLessonEntity[] $lessons
182
     * @param TimetableSupervision[] $supervision
183
     * @param DateTime[] $freeDays
184
     */
185
    private function makeTimetableWeek(WeekOfYear $week, array $lessons, array $supervision, array $freeDays): TimetableWeek {
186
        $timetableWeekEntity = $this->weekRepository->findOneByWeekNumber($week->getWeekNumber());
187
188
        $timetableWeek = new TimetableWeek($week->getYear(), $week->getWeekNumber(), $timetableWeekEntity?->getDisplayName());
189
190
        $lessons = array_filter($lessons, fn(TimetableLessonEntity $lesson) => $this->dateHelper->isBetween($lesson->getDate(), $week->getFirstDay(), $week->getLastDay()));
0 ignored issues
show
It seems like $lesson->getDate() can also be of type null; however, parameter $dateTime of SchulIT\CommonBundle\Hel...DateHelper::isBetween() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
        $lessons = array_filter($lessons, fn(TimetableLessonEntity $lesson) => $this->dateHelper->isBetween(/** @scrutinizer ignore-type */ $lesson->getDate(), $week->getFirstDay(), $week->getLastDay()));
Loading history...
191
192
        $supervision = array_filter($supervision, fn(TimetableSupervision $entry) => $this->dateHelper->isBetween($entry->getDate(), $week->getFirstDay(), $week->getLastDay()));
0 ignored issues
show
It seems like $entry->getDate() can also be of type null; however, parameter $dateTime of SchulIT\CommonBundle\Hel...DateHelper::isBetween() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

192
        $supervision = array_filter($supervision, fn(TimetableSupervision $entry) => $this->dateHelper->isBetween(/** @scrutinizer ignore-type */ $entry->getDate(), $week->getFirstDay(), $week->getLastDay()));
Loading history...
193
194
        for($i = 0; $i < 5; $i++) {
195
            $date = (clone $week->getFirstDay())->modify(sprintf('+%d days', $i));
196
            $isCurrent = $this->isCurrentDay($date);
197
            $isUpcoming = $this->isUpcomingDay($date);
198
            $isFree = $this->isFree($date, $freeDays);
199
200
            $day = $this->makeTimetableDay($date, $isCurrent, $isUpcoming, $isFree, $lessons, $supervision);
201
202
            if($isCurrent || $isUpcoming) {
203
                $timetableWeek->setCurrentOrUpcoming();
204
            }
205
206
            $timetableWeek->days[$i] = $day;
207
        }
208
209
        // Calculate max day lessons
210
        $max = 0;
211
        foreach($timetableWeek->days as $day) {
212
            $lessons = array_keys($day->getLessonsContainers());
213
            if(count($lessons) > 0) {
214
                // max() only works with non-empty arrays
215
                $max = max($max, ...$lessons);
216
            }
217
        }
218
219
        $timetableWeek->setMaxLesson($max);
220
221
        return $timetableWeek;
222
    }
223
224
    /**
225
     * @param TimetableLessonEntity[] $lessons
226
     * @param TimetableSupervision[] $supervision
227
     */
228
    private function makeTimetableDay(DateTime $date, bool $isCurrentDay, bool $isUpcomingDay, bool $isFree, array $lessons, array $supervision): TimetableDay {
229
        $timetableDay = new TimetableDay($date, $isCurrentDay, $isUpcomingDay, $isFree);
230
231
        /** @var TimetableLessonEntity[] $lessons */
232
        $lessons = array_filter($lessons, fn(TimetableLessonEntity $lesson) => $lesson->getDate() == $date);
233
234
        $supervision = array_filter($supervision, fn(TimetableSupervision $entry) => $entry->getDate() == $date);
235
236
        foreach($lessons as $lesson) {
237
            $timetableDay->addTimetableLessonsContainer($lesson);
238
        }
239
240
        foreach($supervision as $entry) {
241
            $timetableDay->addSupervisionEntry($entry);
242
        }
243
244
        return $timetableDay;
245
    }
246
247
    private function isCurrentDay(DateTime $date): bool {
248
        $today = $this->dateHelper->getToday();
249
250
        return $today == $date;
251
    }
252
253
    private function isUpcomingDay(DateTime $date): bool {
254
        $today = $this->dateHelper->getToday();
255
256
        if($this->isWeekend($today) === false) {
257
            return false;
258
        }
259
260
        while($this->isWeekend($today)) {
261
            $today = $today->modify('+1 day');
262
        }
263
264
        return $today == $date;
265
    }
266
267
    /**
268
     * @param DateTime[] $freeDays
269
     */
270
    private function isFree(DateTime $dateTime, array $freeDays): bool {
271
        foreach($freeDays as $freeDay) {
272
            if($dateTime == $freeDay) {
273
                return true;
274
            }
275
        }
276
277
        return false;
278
    }
279
280
    private function isWeekend(DateTime $dateTime): bool {
281
        return $dateTime->format('w') == 0 || $dateTime->format('w') == 6;
282
    }
283
}