TimetableReader::computeLessonStarts()   B
last analyzed

Complexity

Conditions 10
Paths 8

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 25
c 1
b 0
f 0
dl 0
loc 47
rs 7.6666
cc 10
nc 8
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace App\Untis\Html\Timetable;
4
5
use App\Untis\Html\AbstractHtmlReader;
6
use App\Untis\Html\HtmlParseException;
7
use DOMNode;
8
use DOMXPath;
9
use InvalidArgumentException;
10
11
class TimetableReader extends AbstractHtmlReader {
12
    private const ObjectiveSelector = '/html/body/center/font[3]';
13
    private const TableSelector = "//table[@rules='all']";
14
    private const LessonIndicatorCellSelector = './tr[position()>1]/td[1]'; // First tr is the header
15
16
    private const FirstLesson = 1;
17
18
    private array $gradeCellInformation;
19
    private array $subjectCellInformation;
20
21
    public function __construct() {
22
        $this->gradeCellInformation = [
23
            CellInformationType::Weeks, CellInformationType::Subject, CellInformationType::Teacher, CellInformationType::Room
24
        ];
25
26
        $this->subjectCellInformation = [
27
            CellInformationType::Weeks, CellInformationType::Teacher, CellInformationType::Room
28
        ];
29
    }
30
31
    /**
32
     * @throws HtmlParseException
33
     */
34
    public function readHtml(string $html, TimetableType $type): TimetableResult {
35
        $xpath = $this->getXPath($html);
36
        $objective = $this->parseObjective($xpath);
37
        $lessons = $this->parseLessons($xpath, $type);
38
39
        if($type === TimetableType::Grade) {
40
            foreach($lessons as $lesson) {
41
                $lesson->setGrade($objective);
42
            }
43
        } else if($type === TimetableType::Subject) {
44
            foreach($lessons as $lesson) {
45
                $lesson->setSubject($objective);
46
            }
47
        }
48
49
        return new TimetableResult($objective, $lessons);
50
    }
51
52
    /**
53
     * @throws HtmlParseException
54
     */
55
    private function parseObjective(DOMXPath $xpath): string {
56
        $elements = $xpath->query(self::ObjectiveSelector);
57
        $firstElement = $elements !== false ? $elements->item(0) : null;
58
59
        if($firstElement === null) {
60
            throw new HtmlParseException('XPath for objective failed.');
61
        }
62
63
        return trim($firstElement->nodeValue);
64
    }
65
66
    /**
67
     * @return Lesson[]
68
     * @throws HtmlParseException
69
     */
70
    private function parseLessons(DOMXPath $xpath, TimetableType $type): array {
71
        $lessons = [ ];
72
73
        $table = $xpath->query(self::TableSelector);
74
        $table = $table !== false ? $table->item(0) : null;
75
76
        if($table === null) {
77
            throw new HtmlParseException('XPath for lessons table failed.');
78
        }
79
80
        $numberOfLessons = $this->getNumberOfLessons($xpath, $table);
81
        $lessonStarts = $this->computeLessonStarts($xpath, $table, $numberOfLessons);
82
83
        $trNodes = $xpath->query('./tr', $table);
84
        $currentLesson = 0;
85
86
        $cellTypes = $type === TimetableType::Grade ? $this->gradeCellInformation : $this->subjectCellInformation;
87
88
        for($idx = 1; $idx < $trNodes->count(); $idx++) {
89
            $trNode = $trNodes->item($idx);
90
            $tdNodes = $xpath->query('./td', $trNode);
91
92
            if($tdNodes === false || $tdNodes->count() === 0) {
93
                // Every second row is empty/has no child
94
                continue;
95
            }
96
97
            $currentDay = 0;
98
99
            for($tdIdx = 1; $tdIdx < $tdNodes->count(); $tdIdx++) {
100
                $currentDay += $this->computeAdvanceDayCount($lessonStarts, $currentLesson, $currentDay);
101
102
                $lessonStart = $currentLesson;
103
                $numberOfLessonsStartingAtLessonStart = count((array) array_filter($lessonStarts[$currentDay], fn(int $start) => $start === $lessonStart));
104
                $lessonEnd = $currentLesson + $numberOfLessonsStartingAtLessonStart - 1;
105
106
                $lessons = array_merge($lessons, $this->parseLessonsFromCell($xpath, $tdNodes->item($tdIdx), $currentDay, $lessonStart, $lessonEnd, $cellTypes, true));
107
                $currentDay++;
108
            }
109
110
            $currentLesson++;
111
        }
112
113
        return $lessons;
114
    }
115
116
    /**
117
     * @throws HtmlParseException
118
     */
119
    private function getNumberOfLessons(DOMXPath $xpath, DOMNode $table): int {
120
        $tdNodes = $xpath->query(self::LessonIndicatorCellSelector, $table);
121
122
        if($tdNodes === false) {
123
            throw new HtmlParseException('XPath for getting number of lessons failed.');
124
        }
125
126
        return count($tdNodes);
127
    }
128
129
    /**
130
     * @throws HtmlParseException
131
     */
132
    private function computeLessonStarts(DOMXPath $xpath, DOMNode $table, int $numberOfLessons): array {
133
        $trNodes = $xpath->query('./tr', $table);
134
        $lessonStarts = [ ];
135
136
        for($day = 0; $day < 5; $day++) {
137
            $lessonStarts[$day] = range(0, $numberOfLessons - 1);
138
        }
139
140
        if($trNodes === false) {
141
            throw new HtmlParseException('XPath for getting trNodes failed.');
142
        }
143
144
        $lesson = 0;
145
146
        for($idx = 1; $idx < count($trNodes); $idx++) {
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...
147
            $trNode = $trNodes->item($idx);
148
            $tdNodes = $xpath->query('./td', $trNode);
149
150
            if($tdNodes === false || $tdNodes->count() === 0) {
151
                // every second row is empty/has no child
152
                continue;
153
            }
154
155
            $day = 0;
156
157
            for($tdIdx = 1; $tdIdx < $tdNodes->count(); $tdIdx++) {
158
                $tdNode = $tdNodes->item($tdIdx);
159
                if($lessonStarts[$day][$lesson] == $lesson) {
160
                    $rowSpanAttribute = $tdNode->attributes->getNamedItem('rowspan');
0 ignored issues
show
Bug introduced by
The method getNamedItem() does not exist on null. ( Ignorable by Annotation )

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

160
                    /** @scrutinizer ignore-call */ 
161
                    $rowSpanAttribute = $tdNode->attributes->getNamedItem('rowspan');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
161
162
                    if($rowSpanAttribute !== null) {
163
                        $rowSpan = intval($rowSpanAttribute->nodeValue);
164
                        $duration = $rowSpan / 2; // Untis needs two rows per lesson
165
166
                        for($currentLesson = $lesson + 1; $currentLesson < $lesson + $duration; $currentLesson++) {
167
                            $lessonStarts[$day][$currentLesson] = $lesson;
168
                        }
169
                    }
170
                }
171
172
                $day++;
173
            }
174
175
            $lesson++;
176
        }
177
178
        return $lessonStarts;
179
    }
180
181
    private function computeAdvanceDayCount(array $lessonStarts, int $lesson, int $day): int {
182
        if($day > count($lessonStarts)) {
183
            throw new InvalidArgumentException(sprintf('Parameter $day must be less than %d (%d given)', count($lessonStarts), $day));
184
        }
185
186
        if($lesson > (is_countable($lessonStarts[0]) ? count($lessonStarts[0]) : 0)) {
187
            throw new InvalidArgumentException(sprintf('Parameter $lesson mut be less than %d (%d given)', is_countable($lessonStarts[0]) ? count($lessonStarts[0]) : 0, $lesson));
188
        }
189
190
        $advance = 0;
191
192
        for($currentDay = $day; $currentDay < count($lessonStarts); $currentDay++) {
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...
193
            if($lessonStarts[$currentDay][$lesson] != $lesson) {
194
                $advance++;
195
            } else {
196
                break;
197
            }
198
        }
199
200
        return $advance;
201
    }
202
203
    /**
204
     * @param CellInformationType[] $cellTypes
205
     * @return Lesson[]
206
     */
207
    private function parseLessonsFromCell(DOMXPath $xpath, DOMNode $tdNode, int $day, int $lessonStart, int $lessonEnd, array $cellTypes, bool $useWeeks = true): array {
208
        $lessons = [ ];
209
210
        $nodes = $xpath->query('./table/tr', $tdNode);
211
212
        if($nodes === false || $nodes->count() === 0) {
213
            return $lessons;
214
        }
215
216
        for($idx = 0; $idx < $nodes->count(); $idx++) {
217
            $lessonNode = $nodes->item($idx);
218
            $tdNodes = $xpath->query('./td', $lessonNode);
219
220
            if($tdNodes === false || $tdNodes->count() < 2) {
221
                continue;
222
            }
223
224
            $lesson = new Lesson();
225
            $lesson->setLessonStart($lessonStart + self::FirstLesson);
226
            $lesson->setLessonEnd($lessonEnd + self::FirstLesson);
227
            $lesson->setDay($day + 1);
228
229
            for($nodeIdx = 0; $nodeIdx < $tdNodes->count(); $nodeIdx++) {
230
                $value = trim($tdNodes->item($nodeIdx)->nodeValue);
231
                $property = $cellTypes[$useWeeks ? $nodeIdx : $nodeIdx + 1];
232
233
                if($property === CellInformationType::Room) {
234
                    $lesson->setRoom($value);
235
                } else if($property === CellInformationType::Subject) {
236
                    $lesson->setSubject($value);
237
                } else if($property === CellInformationType::Teacher) {
238
                    $lesson->setTeacher($value);
239
                } else if($property === CellInformationType::Weeks) {
240
                    $lesson->setWeeks(explode(',', $value));
241
                }
242
            }
243
244
            $lessons[] = $lesson;
245
        }
246
247
        return $lessons;
248
    }
249
}