SchulIT /
icc
| 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
|
|||||
| 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
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
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 | } |
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: