Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.60% covered (warning)
88.60%
171 / 193
83.33% covered (warning)
83.33%
15 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Exchange
88.60% covered (warning)
88.60%
171 / 193
83.33% covered (warning)
83.33%
15 / 18
113.52
0.00% covered (danger)
0.00%
0 / 1
 getDefaults
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setPeriod
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 addDictionaryEntry
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addDataSet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 withLessData
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getPositionByName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 withCalculatedTotals
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 withMaxByHour
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
8
 withRequestsSum
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 withAverage
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
132
 withUncapturedRequestRowSortedLast
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
9
 withMaxAndAverageFromWaitingTime
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
10
 withWeightedAverageProcessingTime
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
9
 getCalculatedTotals
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 toHashed
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getHashData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
8
 toGrouped
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getGroupedHashSet
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3namespace BO\Zmsentities;
4
5/**
6 * @SuppressWarnings(Complexity)
7 * @SuppressWarnings(PublicMethod)
8 *
9 */
10class Exchange extends Schema\Entity
11{
12    public const string PRIMARY = 'firstDay';
13
14    /**
15     * Statistik CASE labels (must match warehouse SQL in ExchangeRequest* queries).
16     */
17    public const string REQUEST_STAT_NAME_UNCATEGORIZED = 'uncategorized';
18
19    public const string REQUEST_STAT_NAME_NONEXISTENT = 'nonexistent';
20
21    public static $schema = "exchange.json";
22
23    /**
24     * @return (Day|array|string)[]
25     *
26     */
27    #[\Override]
28    public function getDefaults()
29    {
30        return [
31            'firstDay' => new Day(),
32            'lastDay' => new Day(),
33            'period' => 'day',
34            'dictionary' => [ ],
35            'data' => [ ]
36        ];
37    }
38
39    public function setPeriod(\DateTimeInterface $firstDay, \DateTimeInterface $lastDay, $period = 'day'): static
40    {
41        $this->firstDay = (new Day())->setDateTime($firstDay);
42        $this->lastDay = (new Day())->setDateTime($lastDay);
43        $this->period = $period;
44        return $this;
45    }
46
47    public function addDictionaryEntry($variable, $type = 'string', $description = '', $reference = ''): static
48    {
49        $position = count($this['dictionary']);
50        $this['dictionary'][$position] = [
51            'position' => $position,
52            'variable' => $variable,
53            'type' => $type,
54            'description' => $description,
55            'reference' => $reference
56        ];
57        return $this;
58    }
59
60    /**
61     * @param (float|int|string)[] $values
62     *
63     *
64     * @return void
65     */
66    public function addDataSet($values)
67    {
68        if (!is_array($values) && !$values instanceof \Traversable) {
69            throw new \Exception("Values have to be of type array");
70        }
71        if (count($this->dictionary) != count($values)) {
72            throw new \Exception("Mismatching dictionary settings for values (count mismatch)");
73        }
74        $this->data[] = $values;
75    }
76
77    /**
78     * @return static
79     */
80    #[\Override]
81    public function withLessData()
82    {
83        $entity = clone $this;
84        if (isset($entity['firstDay'])) {
85            unset($entity['firstDay']);
86        }
87        if (isset($entity['lastDay'])) {
88            unset($entity['lastDay']);
89        }
90        if (isset($entity['period'])) {
91            unset($entity['period']);
92        }
93        return $entity;
94    }
95
96    public function getPositionByName(string $name)
97    {
98        if (isset($this->dictionary)) {
99            foreach ($this->dictionary as $entry) {
100                if (isset($entry['variable']) && $entry['variable'] == $name) {
101                    return $entry['position'];
102                }
103            }
104        }
105        return false;
106    }
107
108    public function withCalculatedTotals(array $keysToCalculate = ['count'], $dateName = 'name'): static
109    {
110        $entity = clone $this;
111        $namePosition = $this->getPositionByName($dateName);
112        if ($namePosition) {
113            $totals = array_fill(0, count($entity->data[0]), 0);
114            $totals[$namePosition] = 'totals';
115            foreach ($keysToCalculate as $name) {
116                $calculatePosition = $this->getPositionByName($name);
117                foreach ($this->data as $item) {
118                    foreach ($item as $position => $data) {
119                        if (is_numeric($data) && $calculatePosition == $position) {
120                            $totals[$position] += $data;
121                        }
122                    }
123                }
124            }
125            $entity->addDataSet($totals);
126        }
127        return $entity;
128    }
129
130    public function withMaxByHour(array $keysToCalculate = ['count']): static
131    {
132        $entity = clone $this;
133        $maxima = [];
134        foreach ($entity->data as $dateItems) {
135            foreach ($dateItems as $hour => $hourItems) {
136                foreach ($hourItems as $key => $value) {
137                    if (is_numeric($value) && in_array($key, $keysToCalculate)) {
138                        $maxima[$hour][$key] = (
139                          isset($maxima[$hour][$key]) && $maxima[$hour][$key] > $value
140                        ) ? $maxima[$hour][$key] : $value;
141                    }
142                }
143            }
144            $entity->data['max'] = $maxima;
145        }
146        return $entity;
147    }
148
149    public function withRequestsSum($keysToCalculate = ['requestscount']): static
150    {
151        $entity = clone $this;
152        $sum = [];
153        foreach ($entity->data as $name => $entry) {
154            $sum[$name] = 0;
155            foreach ($entry as $dateItem) {
156                foreach ($dateItem as $key => $value) {
157                    if (is_numeric($value) && in_array($key, $keysToCalculate)) {
158                        $sum[$name] += $value;
159                    }
160                }
161            }
162        }
163        $entity->data['sum'] = $sum;
164        return $entity;
165    }
166
167    public function withAverage($keyToCalculate): static
168    {
169        $entity = clone $this;
170        $average = [];
171
172        foreach ($entity->data as $name => $entry) {
173            if (!is_array($entry) && !($entry instanceof \Traversable)) {
174                // Skip or handle non-iterable $entry appropriately.
175                continue;
176            }
177
178            $average[$name . '_sum'] = 0;
179            $average[$name . '_count'] = 0;
180
181            foreach ($entry as $dateItem) {
182                if (!is_array($dateItem) && !($dateItem instanceof \Traversable)) {
183                    // Skip or handle non-iterable $dateItem appropriately.
184                    continue;
185                }
186
187                foreach ($dateItem as $key => $value) {
188                    if (!is_numeric($value) || $key !== $keyToCalculate) {
189                        // Skip non-numeric values or when the key doesn't match.
190                        continue;
191                    }
192
193                    $average[$name . '_sum'] += $value;
194                    $average[$name . '_count']++;
195                }
196            }
197
198            $average[$name] = $average[$name . '_count'] > 0
199                ? round($average[$name . '_sum'] / $average[$name . '_count'], 2)
200                : null;
201        }
202
203        $entity->data['average_' . $keyToCalculate] = $average;
204        return $entity;
205    }
206
207    /**
208     * Sort service rows alphabetically; place synthetic stat rows last
209     * (before aggregated keys sum / average_*).
210     */
211    public function withUncapturedRequestRowSortedLast(): self
212    {
213        $entity = clone $this;
214        if (!is_array($entity->data)) {
215            return $entity;
216        }
217
218        $reserved = ['sum', 'average_processingtime', 'average_processingtime_overall'];
219        $tailStatNames = [
220            self::REQUEST_STAT_NAME_UNCATEGORIZED,
221            self::REQUEST_STAT_NAME_NONEXISTENT,
222        ];
223        $serviceRows = [];
224        foreach ($entity->data as $key => $value) {
225            if (in_array($key, $reserved, true) || in_array($key, $tailStatNames, true)) {
226                continue;
227            }
228            $serviceRows[$key] = $value;
229        }
230
231        uksort($serviceRows, static function ($a, $b) {
232            return strnatcasecmp((string) $a, (string) $b);
233        });
234
235        $ordered = $serviceRows;
236        foreach ($tailStatNames as $tailKey) {
237            if (array_key_exists($tailKey, $entity->data)) {
238                $ordered[$tailKey] = $entity->data[$tailKey];
239            }
240        }
241        foreach ($reserved as $key) {
242            if (array_key_exists($key, $entity->data)) {
243                $ordered[$key] = $entity->data[$key];
244            }
245        }
246        $entity->data = $ordered;
247        return $entity;
248    }
249
250    public function withMaxAndAverageFromWaitingTime(): static
251    {
252        $entity = clone $this;
253        foreach ($entity->data as $date => $dateItems) {
254            $maxima = 0;
255            $total = 0;
256            $count = 0;
257            foreach ($dateItems as $hourItems) {
258                foreach ($hourItems as $key => $value) {
259                    if (is_numeric($value) && 'waitingtime' == $key && 0 < $value) {
260                        $total += $value;
261                        $count += 1;
262                        $maxima = ($maxima > $value) ? $maxima : $value;
263                    }
264                }
265            }
266            $entity->data[$date]['max'] = $maxima;
267            $entity->data[$date]['average'] = (! $total || ! $count) ? 0 : floor($total / $count);
268        }
269        return $entity;
270    }
271
272    public function withWeightedAverageProcessingTime(): static
273    {
274        $entity = clone $this;
275        $weightedSum = 0.0;
276        $totalCount = 0;
277        $excludedNames = [
278            self::REQUEST_STAT_NAME_UNCATEGORIZED,
279            self::REQUEST_STAT_NAME_NONEXISTENT,
280            'sum',
281            'average_processingtime',
282            'average_processingtime_overall',
283        ];
284
285        foreach ($entity->data as $name => $dateItems) {
286            if (in_array($name, $excludedNames, true) || !is_iterable($dateItems)) {
287                continue;
288            }
289
290            foreach ($dateItems as $dateItem) {
291                $processingTime = $dateItem['processingtime'] ?? null;
292                $requestCount = $dateItem['requestscount'] ?? null;
293                if (!is_numeric($processingTime) || !is_numeric($requestCount) || $requestCount <= 0) {
294                    continue;
295                }
296
297                $weightedSum += (float) $processingTime * (int) $requestCount;
298                $totalCount += (int) $requestCount;
299            }
300        }
301        $entity->data['average_processingtime_overall'] = $totalCount > 0 ? round($weightedSum / $totalCount, 2) : null;
302
303        return $entity;
304    }
305
306    public function getCalculatedTotals()
307    {
308        foreach (array_reverse($this->data) as $item) {
309            foreach ($item as $data) {
310                if ($data == 'totals') {
311                    return $item;
312                }
313            }
314        }
315        return null;
316    }
317
318    public function toHashed(array $hashfields = []): static
319    {
320        $entity = clone $this;
321        $entity->data = $this->getHashData($hashfields);
322        unset($entity->dictionary);
323        return $entity;
324    }
325
326    /**
327     * @return (array|mixed)[]|false
328     *
329     */
330    public function getHashData(array $hashfields = [], bool $first = false): array|false
331    {
332        $hash = [];
333        foreach ($this->dictionary as $entry) {
334            foreach ($this->data as $key => $item) {
335                foreach ($item as $position => $data) {
336                    if ($entry['position'] == $position) {
337                        if (count($hashfields) && in_array($entry['variable'], $hashfields)) {
338                            $hash[$key][$entry['variable']] = $data;
339                        } else {
340                            $hash[$key][$entry['variable']] = $data;
341                        }
342                    }
343                }
344            }
345        }
346        return ($first) ? reset($hash) : $hash;
347    }
348
349    public function toGrouped(array $fields, array $hashfields): static
350    {
351        $entity = clone $this;
352        $entity->data = $this->getGroupedHashSet($fields, $hashfields);
353        unset($entity->dictionary);
354        return $entity;
355    }
356
357    public function getGroupedHashSet(array $fields, array $hashfields)
358    {
359        $list = [];
360        if (count($fields)) {
361            $field = array_shift($fields);
362            $fieldposition = $this->getPositionByName($field);
363
364            $requestscountPosition = $this->getPositionByName('requestscount');
365
366
367            foreach ($this->data as $element) {
368                if (isset($element[$fieldposition])) {
369                    if (!isset($list[$element[$fieldposition]])) {
370                        $list[$element[$fieldposition]] = clone $this;
371                        $list[$element[$fieldposition]]->data = [];
372                    }
373                    if ($requestscountPosition !== false && isset($element[$requestscountPosition])) {
374                        $element[$requestscountPosition] = (int) $element[$requestscountPosition];
375                    }
376
377                    $list[$element[$fieldposition]]->data[] = $element;
378                }
379            }
380
381            foreach ($list as $key => $row) {
382                if ($row instanceof Exchange) {
383                    $list[$key] = $row->getGroupedHashSet($fields, $hashfields);
384                }
385            }
386        } else {
387            return $this->getHashData($hashfields, true);
388        }
389        return $list;
390    }
391}