Passed
Push — master ( 2b1200...dfc48c )
by Victor
01:25
created

IntervalGraph::truncate()   C

Complexity

Conditions 13
Paths 16

Size

Total Lines 58
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 29
nc 16
nop 4
dl 0
loc 58
rs 6.6166
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A IntervalGraph::draw() 0 13 2

How to fix   Long Method    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 Vctls\IntervalGraph;
4
5
/**
6
 * A class to manipulate and display arrays of weighted intervals.
7
 */
8
class IntervalGraph implements \JsonSerializable
9
{
10
    use TruncatableTrait;
11
    
12
    /** @var array Initial intervals */
13
    protected $intervals;
14
15
    /** @var array Processed values */
16
    protected $values;
17
18
    /** @var string Path to the template used for rendering. */
19
    protected $template = 'template.php';
20
21
    /** @var \Closure Return a numeric value from the inital bound value. */
22
    protected $boundToNumeric;
23
24
    /** @var \Closure Return a string from the initial bound value. */
25
    protected $boundToString;
26
27
    /** @var \Closure Return a numeric value from an initial interval value. */
28
    protected $valueToNumeric;
29
30
    /** @var \Closure Return a string value from an initial interval value. */
31
    protected $valueToString;
32
33
    /** @var \Closure Aggregate interval values. */
34
    protected $aggregateFunction;
35
36
    /** @var Palette */
37
    protected $palette;
38
39
    /**
40
     * Create an IntervalGraph from intervals carrying values.
41
     *
42
     * @param array[] $intervals An array of intervals,
43
     * with a low bound, high bound and a value.
44
     */
45
    public function __construct($intervals = null)
46
    {
47
        if (isset($intervals)) {
48
            $this->setIntervals($intervals);
49
        }
50
51
        $this->boundToNumeric = function (\DateTime $bound) {
52
            return $bound->getTimestamp();
53
        };
54
55
        $this->boundToString = function (\DateTime $bound) {
56
            return $bound->format("Y-m-d");
57
        };
58
59
        $this->valueToNumeric = function ($v) {
60
            return $v === null ? null : (int)($v * 100);
61
        };
62
63
        $this->valueToString = function ($v) {
64
            return $v === null ? null : ($v * 100 . '%');
65
        };
66
67
        $this->aggregateFunction = function ($a, $b) {
68
            if ($a === null && $b === null) {
69
                return null;
70
            }
71
            return round($a + $b, 2);
72
        };
73
74
        $this->palette = new Palette();
75
    }
76
77
    /**
78
     * Check that an array of intervals is correctly formatted.
79
     *
80
     * The first element must be the low bound.
81
     *
82
     * The second element must be the high bound.
83
     *
84
     * The third element must be the value.
85
     *
86
     * Inverted end and low bounds will be put back in chronological order.
87
     *
88
     * @return $this
89
     */
90
    public function checkIntervals()
91
    {
92
93
        foreach ($this->intervals as $intervalKey => $interval) {
94
95
            // Check that the interval is an array.
96
            if (!is_array($interval)) {
97
                $t = gettype($interval);
98
                throw new \InvalidArgumentException(
99
                    "Each element of the '\$intervals' array should be an array, $t given."
100
                );
101
            }
102
103
            // Check that the bounds and value of the interval can be converted to both a numeric
104
            // and string value with the given closures.
105
            foreach ([['Lower bound', 'bound'], ['Higher bound', 'bound'], ['Value', 'value']] as $index => $property) {
106
107
                // Skip value property of valueless intervals.
108
                if ($property[1] === 'value' && !isset($interval[$index])) {
109
                    continue;
110
                }
111
112
                foreach (['numeric', 'string'] as $expectedType) {
113
114
                    $expectedTypeTitle = ucfirst($expectedType);
115
116
                    try {
117
                        $value = ($this->{"$property[1]To$expectedTypeTitle"})($interval[$index]);
118
                    } catch (\Exception $exception) {
119
                        // FIXME Handle Type errors?
120
                        throw new PropertyConversionException(
121
                            "$property[0] of interval $intervalKey cannot be converted to a $expectedType value " .
122
                            "with the given '$property[1]To$expectedTypeTitle' function. Error : " . 
123
                            $exception->getMessage()
124
                        );
125
                    }
126
127
                    $actualType = gettype($value);
128
129
                    if (!call_user_func("is_$expectedType", $value)) {
130
                        throw new PropertyConversionException(
131
                            "$property[0] of interval $intervalKey is not converted to a $expectedType value " .
132
                            "by the given '$property[1]To$expectedTypeTitle' function. Returned type : $actualType"
133
                        );
134
                    }
135
                }
136
            }
137
138
            // Ensure start and high bounds are in the right order.
139
            if ($interval[0] > $interval [1]) {
140
                $a = $interval[0];
141
                $intervals[$intervalKey][0] = $interval[1];
142
                $intervals[$intervalKey][1] = $a;
143
            }
144
        }
145
146
        // TODO Check that the values can be aggregated with the given closure.
147
148
        return $this;
149
    }
150
    
151
    /**
152
     * Render an HTML view of the intervalGraph.
153
     *
154
     * @return string
155
     */
156
    public function __toString()
157
    {
158
        try {
159
            $html = $this->draw();
160
        } catch (\Exception $e) {
161
            $html = "Error : " . $e->getMessage();
162
        }
163
        return $html;
164
    }
165
166
    /**
167
     * Render an HTML view of the intervalGraph.
168
     *
169
     * @return string
170
     */
171
    public function draw()
172
    {
173
        if (!isset($this->values)) {
174
            $this->process();
175
        }
176
        $vs = $this->values;
177
        ob_start();
178
        include $this->template;
179
180
        // Remove all surplus whitespace.
181
        return preg_replace(
182
            ['/(?<=>)\s+/', '/\s+(?=<)/', '/\s+/'], ['', '', ' '],
183
            ob_get_clean()
184
        );
185
    }
186
187
    /**
188
     * Process intervals and store processed values.
189
     *
190
     * @return IntervalGraph
191
     */
192
    public function process()
193
    {
194
        $flatIntervals = $this->getFlatIntervals();
195
196
        // Extract values.
197
        $t = array_column($flatIntervals, 2);
198
199
        // Change bounds to numeric values.
200
        $numVals = array_map(function (array $i) {
201
            return [
202
                ($this->boundToNumeric)($i[0]),
203
                ($this->boundToNumeric)($i[1]),
204
            ];
205
        }, $flatIntervals);
206
207
        // Order by low bound.
208
        uasort($numVals, function (array $i1, array $i2) {
209
            return ($i1[0] < $i2[0]) ? -1 : 1;
210
        });
211
212
        // Get the min timestamp.
213
        $min = reset($numVals)[0];
214
215
        // Substract min from all timestamps.
216
        $numVals = array_map(function ($i) use ($min) {
217
            return [
218
                $i[0] - $min,
219
                $i[1] - $min
220
            ];
221
        }, $numVals);
222
223
        // Order by high bound.
224
        uasort($numVals, function (array $i1, array $i2) {
225
            return ($i1[1] < $i2[1]) ? -1 : 1;
226
        });
227
228
        // Get max timestamp.
229
        $max = end($numVals)[1];
230
231
        // Calculate percentages.
232
        $numVals = array_map(function (array $i) use ($max) {
233
            return array_map(function ($int) use ($max) {
234
                return round($int * 100 / $max);
235
            }, $i);
236
        }, $numVals);
237
238
        // Put values back in, along with the formatted bound.
239
        // Since we're using associative sorting functions, we know the keys haven't changed.
240
        $numVals = array_map(function ($k, array $i) use ($t, $flatIntervals) {
241
            if ($flatIntervals[$k][0] === $flatIntervals[$k][1]) {
242
                return [
243
                    $i[0], // Single value position percentage
244
                    ($this->boundToString)($flatIntervals[$k][0]), // Signle value string
245
                ];
246
            } else {
247
                $colorval = isset($t[$k]) ? ($this->valueToNumeric)($t[$k]) : null;
248
                $stingval = isset($t[$k]) ? ($this->valueToString)($t[$k]) : null;
249
                return [
250
                    $i[0], // Interval start percentage
251
                    100 - $i[1], // Interval end percentage from right
252
                    // Note: for some reason, using 'widht' instead of 'right'
253
                    // causes the right border to be hidden underneath the next interval.
254
                    !empty($t) ? $this->palette->getColor($colorval) : 50, // Interval color
255
                    ($this->boundToString)($flatIntervals[$k][0]), // Interval start string value
256
                    ($this->boundToString)($flatIntervals[$k][1]), // Interval end string value
257
                    !empty($t) ? ($stingval) : null,// Interval string value
258
                ];
259
            }
260
        }, array_keys($numVals), $numVals);
261
262
        // Put discrete values at the end and reset indices.
263
        // Reseting indices ensures the processed values are
264
        // serialized as correctly ordered JSON arrays.
265
        usort($numVals, function ($i) {
266
            return count($i) === 2 ? 1 : -1;
267
        });
268
269
        $this->values = $numVals;
270
271
        return $this;
272
    }
273
274
    /**
275
     * Transform an array of intervals with possible overlapping
276
     * into an array of adjacent intervals with no overlapping.
277
     *
278
     * @return array
279
     */
280
    public function getFlatIntervals()
281
    {
282
        $discreteValues = self::extractDiscreteValues($this->intervals);
283
        $signedBounds = self::intervalsToSignedBounds($this->intervals);
284
        $adjacentIntervals = $this->calcAdjacentIntervals($signedBounds);
285
286
        // Remove empty interval generated when two or more intervals share a common bound.
287
        $adjacentIntervals = array_values(array_filter($adjacentIntervals, function ($i) {
288
            // Use weak comparison in case of object typed bounds.
289
            return $i[0] != $i[1];
290
        }));
291
292
        // Push discrete values back into the array.
293
        if (!empty($discreteValues)) {
294
            array_push($adjacentIntervals, ...$discreteValues);
295
        }
296
297
        return $adjacentIntervals;
298
    }
299
300
    /**
301
     * Extract discrete values from an array of intervals.
302
     *
303
     * Intervals with the exact same lower and higher bound will be considered as discrete values.
304
     *
305
     * They will be removed from the initial array, and returned in a separate array.
306
     *
307
     * @param array $intervals The initial array.
308
     * @return array An array containing only discrete values.
309
     */
310
    public static function extractDiscreteValues(array &$intervals)
311
    {
312
        $discreteValues = array_filter($intervals, function ($interval) {
313
            return $interval[0] === $interval[1];
314
        });
315
316
        $intervals = array_diff_key($intervals, $discreteValues);
317
318
        return $discreteValues;
319
    }
320
321
    /**
322
     * Make an array of bounds from an array of intervals.
323
     *
324
     * Assign the value of the interval to each bound.
325
     *
326
     * Assign and a '+' sign if it is a low bound, and a '-' if it is an high bound.
327
     *
328
     * @param $intervals
329
     * @return array
330
     */
331
    public static function intervalsToSignedBounds($intervals)
332
    {
333
        $bounds = [];
334
        foreach ($intervals as $key => $interval) {
335
            $bounds[] = [$interval[0], isset($interval[2]) ? $interval[2] : null, '+', $key];
336
            $bounds[] = [$interval[1], isset($interval[2]) ? $interval[2] : null, '-', $key];
337
        }
338
        // Order the bounds.
339
        usort($bounds, function (array $d1, array $d2) {
340
            return ($d1[0] < $d2[0]) ? -1 : 1;
341
        });
342
        return $bounds;
343
    }
344
345
    /**
346
     * Create each new interval and calculate its value based on the active intervals on each bound.
347
     *
348
     * @param $bounds
349
     * @return array
350
     */
351
    public function calcAdjacentIntervals($bounds)
352
    {
353
        // Get the values of the original intervals, including nulls.
354
        $origIntVals = array_map(function ($interval) {
355
            return isset($interval[2]) ? $interval[2] : null;
356
        }, $this->intervals);
357
358
        $newIntervals = [];
359
        $activeIntervals = [];
360
361
        // Create new intervals for each set of two consecutive bounds,
362
        // and calculate its total value.
363
        for ($i = 1; $i < count($bounds); $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...
364
365
            // Set the current bound.
366
            $curBound = $bounds[$i - 1];
367
368
            if ($curBound[2] === '+') {
369
                // If this is a low bound,
370
                // add the key of the interval to the array of active intervals.
371
                $activeIntervals[$curBound[3]] = true;
372
            } else {
373
                // If this is an high bound, remove the key.
374
                unset($activeIntervals[$curBound[3]]);
375
            }
376
377
            if (empty($activeIntervals)) {
378
                // If no intervals are active on this bound,
379
                // the value of this interval is null.
380
                $ival = null;
381
            } else {
382
                // Else, aggregate the values of the corresponding intervals.
383
                $ival = array_reduce(
384
                    array_intersect_key($origIntVals, $activeIntervals),
385
                    $this->aggregateFunction
386
                );
387
            }
388
389
            $newIntervals[] = [$curBound[0], $bounds[$i][0], $ival];
390
        }
391
392
        return $newIntervals;
393
    }
394
395
    /**
396
     * Define the function to convert the interval values to a numeric value
397
     * in order to match them to a color on the palette.
398
     *
399
     * @param \Closure $valueToNumeric
400
     * @return IntervalGraph
401
     */
402
    public function setValueToNumeric(\Closure $valueToNumeric)
403
    {
404
        $this->valueToNumeric = $valueToNumeric;
405
        return $this;
406
    }
407
408
    /**
409
     * Define the  function to convert the interval values to strings
410
     * in order to display them in the view.
411
     *
412
     * @param \Closure $valueToString
413
     * @return IntervalGraph
414
     */
415
    public function setValueToString(\Closure $valueToString)
416
    {
417
        $this->valueToString = $valueToString;
418
        return $this;
419
    }
420
421
    /**
422
     * Define the function to aggregate interval values.
423
     *
424
     * @param \Closure $aggregate
425
     * @return IntervalGraph
426
     */
427
    public function setAggregate(\Closure $aggregate)
428
    {
429
        $this->aggregateFunction = $aggregate;
430
        return $this;
431
    }
432
433
    /**
434
     * Set the function to convert interval bound values to string.
435
     *
436
     * @param \Closure $boundToString
437
     * @return IntervalGraph
438
     */
439
    public function setBoundToString($boundToString)
440
    {
441
        $this->boundToString = $boundToString;
442
        return $this;
443
    }
444
445
    /**
446
     * @return array
447
     */
448
    public function getIntervals()
449
    {
450
        return $this->intervals;
451
    }
452
453
    /**
454
     * Set the intervals to be processed.
455
     *
456
     * If another set of intervals was previously processed,
457
     * the processed values will be deleted.
458
     *
459
     * @param array $intervals
460
     * @return IntervalGraph
461
     */
462
    public function setIntervals(array $intervals)
463
    {
464
        $this->intervals = $intervals;
465
        $this->values = null;
466
        return $this;
467
    }
468
469
    /**
470
     * @return array
471
     */
472
    public function getValues()
473
    {
474
        return $this->values;
475
    }
476
477
    /**
478
     * @return string
479
     */
480
    public function getTemplate()
481
    {
482
        return $this->template;
483
    }
484
485
    /**
486
     * Set the PHP template to use for rendering.
487
     *
488
     * @param string $template
489
     * @return IntervalGraph
490
     */
491
    public function setTemplate($template)
492
    {
493
        $this->template = $template;
494
        return $this;
495
    }
496
497
    /**
498
     * @return Palette
499
     */
500
    public function getPalette()
501
    {
502
        return $this->palette;
503
    }
504
505
    /**
506
     * @param Palette $palette
507
     * @return IntervalGraph
508
     */
509
    public function setPalette($palette)
510
    {
511
        $this->palette = $palette;
512
        return $this;
513
    }
514
515
    /**
516
     * @param \Closure $boundToNumeric
517
     * @return IntervalGraph
518
     */
519
    public function setBoundToNumeric($boundToNumeric)
520
    {
521
        $this->boundToNumeric = $boundToNumeric;
522
        return $this;
523
    }
524
525
    /**
526
     * Return the array of values to be serialized by json_encode.
527
     *
528
     * @return array
529
     */
530
    public function jsonSerialize()
531
    {
532
        if (!isset($this->values)) {
533
            $this->process();
534
        }
535
        return $this->values;
536
    }
537
}