Completed
Push — master ( 82c71b...47eb96 )
by ignace nyamagana
03:20
created

Sequence   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 573
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 140
dl 0
loc 573
ccs 185
cts 185
cp 1
rs 2
c 0
b 0
f 0
wmc 80

38 Methods

Rating   Name   Duplication   Size   Complexity  
A sortByStartDate() 0 3 1
A __construct() 0 3 1
A gaps() 0 20 6
A boundaries() 0 8 2
B intersections() 0 27 7
A clear() 0 3 1
A reduce() 0 7 2
A getIntersections() 0 3 1
A getIterator() 0 4 2
A remove() 0 8 1
A unshift() 0 3 1
A substract() 0 15 4
A set() 0 4 1
A filter() 0 8 2
A indexOf() 0 9 3
A offsetExists() 0 3 1
A getGaps() 0 3 1
A jsonSerialize() 0 3 1
A contains() 0 10 3
A offsetGet() 0 3 1
A insert() 0 8 3
A getBoundaries() 0 3 1
A map() 0 15 3
A get() 0 8 2
A unions() 0 12 2
A substractOne() 0 15 5
A count() 0 3 1
A sort() 0 3 1
A isEmpty() 0 3 1
A some() 0 9 3
A calculateUnion() 0 19 4
A toArray() 0 3 1
A offsetUnset() 0 3 1
A offsetSet() 0 8 2
A getTotalTimestampInterval() 0 8 2
A every() 0 9 3
A sorted() 0 9 2
A push() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Sequence often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Sequence, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use ArrayAccess;
17
use Countable;
18
use Iterator;
19
use IteratorAggregate;
20
use JsonSerializable;
21
use function array_filter;
22
use function array_merge;
23
use function array_splice;
24
use function array_unshift;
25
use function array_values;
26
use function count;
27
use function reset;
28
use function sort;
29
use function sprintf;
30
use function uasort;
31
use function usort;
32
use const ARRAY_FILTER_USE_BOTH;
33
34
/**
35
 * A class to manipulate interval collection.
36
 *
37
 * @package League.period
38
 * @author  Ignace Nyamagana Butera <[email protected]>
39
 * @since   4.1.0
40
 */
41
final class Sequence implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
42
{
43
    /**
44
     * @var Period[]
45
     */
46
    private $intervals = [];
47
48
    /**
49
     * new instance.
50
     *
51
     * @param Period ...$intervals
52
     */
53 117
    public function __construct(Period ...$intervals)
54
    {
55 117
        $this->intervals = $intervals;
56 117
    }
57
58
    /**
59
     * Returns the sequence boundaries as a Period instance.
60
     *
61
     * If the sequence contains no interval null is returned.
62
     *
63
     * @return ?Period
64
     */
65 27
    public function boundaries(): ?Period
66
    {
67 27
        $period = reset($this->intervals);
68 27
        if (false === $period) {
69 6
            return null;
70
        }
71
72 27
        return $period->merge(...$this->intervals);
73
    }
74
75
    /**
76
     * Returns the gaps inside the instance.
77
     */
78 9
    public function gaps(): self
79
    {
80 9
        $sequence = new self();
81 9
        $interval = null;
82 9
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
83 9
            if (null === $interval) {
84 9
                $interval = $period;
85 9
                continue;
86
            }
87
88 9
            if (!$interval->overlaps($period) && !$interval->abuts($period)) {
89 6
                $sequence->push($interval->gap($period));
90
            }
91
92 9
            if (!$interval->contains($period)) {
93 7
                $interval = $period;
94
            }
95
        }
96
97 9
        return $sequence;
98
    }
99
100
    /**
101
     * Sorts two Interval instance using their start datepoint.
102
     */
103 15
    private function sortByStartDate(Period $interval1, Period $interval2): int
104
    {
105 15
        return $interval1->getStartDate() <=> $interval2->getStartDate();
106
    }
107
108
    /**
109
     * Substract a Sequence from the current instance.
110
     */
111 15
    public function substract(Sequence $sequence): self
112
    {
113 15
        if ($this->isEmpty()) {
114 3
            return $this;
115
        }
116
117 15
        if ($sequence->isEmpty()) {
118 3
            return $this;
119
        }
120
121 12
        if (!$this->boundaries()->overlaps($sequence->boundaries())) {
0 ignored issues
show
Bug introduced by
It seems like $sequence->boundaries() can also be of type null; however, parameter $interval of League\Period\Period::overlaps() does only seem to accept League\Period\Period, 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

121
        if (!$this->boundaries()->overlaps(/** @scrutinizer ignore-type */ $sequence->boundaries())) {
Loading history...
122 3
            return $this;
123
        }
124
125 9
        return $sequence->reduce([$this, 'substractOne'], $this);
126
    }
127
128
    /**
129
     * Substract an Interval from a Sequence.
130
     */
131 9
    private function substractOne(Sequence $sequence, Period $interval): self
132
    {
133 9
        if ($sequence->isEmpty() || !$sequence->boundaries()->overlaps($interval)) {
134 3
            return $sequence;
135
        }
136
        
137 9
        $new = new Sequence();
138 9
        foreach ($sequence as $period) {
139 9
            $substract = $period->substract($interval);
140 9
            if (!$substract->isEmpty()) {
141 5
                $new->push(...$substract);
142
            }
143
        }
144
145 9
        return $new;
146
    }
147
148
    /**
149
     * Returns the intersections inside the instance.
150
     */
151 9
    public function intersections(): self
152
    {
153 9
        $sequence = new self();
154 9
        $current = null;
155 9
        $isPreviouslyContained = false;
156 9
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
157 9
            if (null === $current) {
158 9
                $current = $period;
159 9
                continue;
160
            }
161
162 9
            $isContained = $current->contains($period);
163 9
            if ($isContained && $isPreviouslyContained) {
164 3
                continue;
165
            }
166
167 9
            if ($current->overlaps($period)) {
168 6
                $sequence->push($current->intersect($period));
169
            }
170
171 9
            $isPreviouslyContained = $isContained;
172 9
            if (!$isContained) {
173 7
                $current = $period;
174
            }
175
        }
176
177 9
        return $sequence;
178
    }
179
180
    /**
181
     * Returns the unions inside the instance.
182
     */
183 6
    public function unions(): self
184
    {
185
        $sequence = $this
186 6
            ->sorted([$this, 'sortByStartDate'])
187 6
            ->reduce([$this, 'calculateUnion'], new self())
188
        ;
189
190 6
        if ($sequence->intervals === $this->intervals) {
191 3
            return $this;
192
        }
193
194 3
        return $sequence;
195
    }
196
197
    /**
198
     * Iteratively calculate the union sequence.
199
     */
200 6
    private function calculateUnion(Sequence $sequence, Period $period): Sequence
201
    {
202 6
        if ($sequence->isEmpty()) {
203 6
            $sequence->push($period);
204
205 6
            return $sequence;
206
        }
207
208 3
        $index = $sequence->count() - 1;
209 3
        $interval = $sequence->get($index);
210 3
        if ($interval->overlaps($period) || $interval->abuts($period)) {
211 3
            $sequence->set($index, $interval->merge($period));
212
213 3
            return $sequence;
214
        }
215
216 3
        $sequence->push($period);
217
218 3
        return $sequence;
219
    }
220
221
    /**
222
     * Returns the sequence boundaries as a Period instance.
223
     *
224
     * DEPRECATION WARNING! This method will be removed in the next major point release
225
     *
226
     * @deprecated deprecated since version 4.4.0
227
     * @see        ::boundaries
228
     *
229
     * If the sequence contains no interval null is returned.
230
     *
231
     * @return ?Period
232
     */
233 12
    public function getBoundaries(): ?Period
234
    {
235 12
        return $this->boundaries();
236
    }
237
238
    /**
239
     * Returns the intersections inside the instance.
240
     *
241
     * DEPRECATION WARNING! This method will be removed in the next major point release
242
     *
243
     * @deprecated deprecated since version 4.4.0
244
     * @see        ::intersections
245
     */
246 9
    public function getIntersections(): self
247
    {
248 9
        return $this->intersections();
249
    }
250
251
    /**
252
     * Returns the gaps inside the instance.
253
     *
254
     * DEPRECATION WARNING! This method will be removed in the next major point release
255
     *
256
     * @deprecated deprecated since version 4.4.0
257
     * @see        ::gaps
258
     */
259 9
    public function getGaps(): self
260
    {
261 9
        return $this->gaps();
262
    }
263
264
    /**
265
     * Returns the sum of all instances durations as expressed in seconds.
266
     */
267 3
    public function getTotalTimestampInterval(): float
268
    {
269 3
        $retval = 0;
270 3
        foreach ($this->intervals as $interval) {
271 3
            $retval += $interval->getTimestampInterval();
272
        }
273
274 3
        return $retval;
275
    }
276
277
    /**
278
     * Tells whether some intervals in the current instance satisfies the predicate.
279
     */
280 3
    public function some(callable $predicate): bool
281
    {
282 3
        foreach ($this->intervals as $offset => $interval) {
283 3
            if (true === $predicate($interval, $offset)) {
284 3
                return true;
285
            }
286
        }
287
288 3
        return false;
289
    }
290
291
    /**
292
     * Tells whether all intervals in the current instance satisfies the predicate.
293
     */
294 3
    public function every(callable $predicate): bool
295
    {
296 3
        foreach ($this->intervals as $offset => $interval) {
297 3
            if (true !== $predicate($interval, $offset)) {
298 3
                return false;
299
            }
300
        }
301
302 3
        return [] !== $this->intervals;
303
    }
304
305
    /**
306
     * Returns the array representation of the sequence.
307
     *
308
     * @return Period[]
309
     */
310 6
    public function toArray(): array
311
    {
312 6
        return $this->intervals;
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318 3
    public function jsonSerialize(): array
319
    {
320 3
        return $this->intervals;
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326 27
    public function getIterator(): Iterator
327
    {
328 27
        foreach ($this->intervals as $offset => $interval) {
329 27
            yield $offset => $interval;
330
        }
331 27
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336 54
    public function count(): int
337
    {
338 54
        return count($this->intervals);
339
    }
340
341
    /**
342
     * @inheritdoc
343
     *
344
     * @param mixed $offset the index of the Period instance to validate.
345
     */
346 3
    public function offsetExists($offset): bool
347
    {
348 3
        return isset($this->intervals[$offset]);
349
    }
350
351
    /**
352
     * @inheritdoc
353
     * @see ::get
354
     *
355
     * @param mixed $offset the index of the Period instance to retrieve.
356
     */
357 12
    public function offsetGet($offset): Period
358
    {
359 12
        return $this->get($offset);
360
    }
361
362
    /**
363
     * @inheritdoc
364
     * @see ::remove
365
     *
366
     * @param mixed $offset the index of the Period instance to remove.
367
     */
368 6
    public function offsetUnset($offset): void
369
    {
370 6
        $this->remove($offset);
371 3
    }
372
373
    /**
374
     * @inheritdoc
375
     * @see ::set
376
     * @see ::push
377
     *
378
     * @param mixed $offset   the index of the Period to add or update.
379
     * @param mixed $interval the Period instance to add.
380
     */
381 9
    public function offsetSet($offset, $interval): void
382
    {
383 9
        if (null !== $offset) {
384 9
            $this->set($offset, $interval);
385 3
            return;
386
        }
387
388 3
        $this->push($interval);
389 3
    }
390
391
    /**
392
     * Tells whether the sequence is empty.
393
     */
394 30
    public function isEmpty(): bool
395
    {
396 30
        return [] === $this->intervals;
397
    }
398
399
    /**
400
     * Tells whether the given interval is present in the sequence.
401
     *
402
     * @param Period ...$intervals
403
     */
404 6
    public function contains(Period $interval, Period ...$intervals): bool
405
    {
406 6
        $intervals[] = $interval;
407 6
        foreach ($intervals as $period) {
408 6
            if (false === $this->indexOf($period)) {
409 4
                return false;
410
            }
411
        }
412
413 6
        return true;
414
    }
415
416
    /**
417
     * Attempts to find the first offset attached to the submitted interval.
418
     *
419
     * If no offset is found the method returns boolean false.
420
     *
421
     * @return int|bool
422
     */
423 6
    public function indexOf(Period $interval)
424
    {
425 6
        foreach ($this->intervals as $offset => $period) {
426 6
            if ($period->equals($interval)) {
427 6
                return $offset;
428
            }
429
        }
430
431 6
        return false;
432
    }
433
434
    /**
435
     * Returns the interval specified at a given offset.
436
     *
437
     * @throws InvalidIndex If the offset is illegal for the current sequence
438
     */
439 54
    public function get(int $offset): Period
440
    {
441 54
        $period = $this->intervals[$offset] ?? null;
442 54
        if (null !== $period) {
443 45
            return $period;
444
        }
445
446 15
        throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
447
    }
448
449
    /**
450
     * Sort the current instance according to the given comparison callable
451
     * and maintain index association.
452
     *
453
     * Returns true on success or false on failure
454
     */
455 6
    public function sort(callable $compare): bool
456
    {
457 6
        return uasort($this->intervals, $compare);
458
    }
459
460
    /**
461
     * Adds new intervals at the front of the sequence.
462
     *
463
     * The sequence is re-indexed after addition
464
     *
465
     * @param Period ...$intervals
466
     */
467 3
    public function unshift(Period $interval, Period ...$intervals): void
468
    {
469 3
        $this->intervals = array_merge([$interval], $intervals, $this->intervals);
470 3
    }
471
472
    /**
473
     * Adds new intervals at the end of the sequence.
474
     *
475
     * @param Period ...$intervals
476
     */
477 27
    public function push(Period $interval, Period ...$intervals): void
478
    {
479 27
        $this->intervals = array_merge($this->intervals, [$interval], $intervals);
480 27
    }
481
482
    /**
483
     * Inserts new intervals at the specified offset of the sequence.
484
     *
485
     * The sequence is re-indexed after addition
486
     *
487
     * @param Period ...$intervals
488
     *
489
     * @throws InvalidIndex If the offset is illegal for the current sequence.
490
     */
491 3
    public function insert(int $offset, Period $interval, Period ...$intervals): void
492
    {
493 3
        if ($offset < 0 || $offset > count($this->intervals)) {
494 3
            throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
495
        }
496
497 3
        array_unshift($intervals, $interval);
498 3
        array_splice($this->intervals, $offset, 0, $intervals);
499 3
    }
500
501
    /**
502
     * Updates the interval at the specify offset.
503
     *
504
     * @throws InvalidIndex If the offset is illegal for the current sequence.
505
     */
506 12
    public function set(int $offset, Period $interval): void
507
    {
508 12
        $this->get($offset);
509 9
        $this->intervals[$offset] = $interval;
510 9
    }
511
512
    /**
513
     * Removes an interval from the sequence at the given offset and returns it.
514
     *
515
     * The sequence is re-indexed after removal
516
     *
517
     * @throws InvalidIndex If the offset is illegal for the current sequence.
518
     */
519 9
    public function remove(int $offset): Period
520
    {
521 9
        $interval = $this->get($offset);
522 6
        unset($this->intervals[$offset]);
523
524 6
        $this->intervals = array_values($this->intervals);
525
526 6
        return $interval;
527
    }
528
529
    /**
530
     * Filters the sequence according to the given predicate.
531
     *
532
     * This method MUST retain the state of the current instance, and return
533
     * an instance that contains the interval which validate the predicate.
534
     */
535 6
    public function filter(callable $predicate): self
536
    {
537 6
        $intervals = array_filter($this->intervals, $predicate, ARRAY_FILTER_USE_BOTH);
538 6
        if ($intervals === $this->intervals) {
539 3
            return $this;
540
        }
541
542 3
        return new self(...$intervals);
543
    }
544
545
    /**
546
     * Removes all intervals from the sequence.
547
     */
548 3
    public function clear(): void
549
    {
550 3
        $this->intervals = [];
551 3
    }
552
553
    /**
554
     * Returns an instance sorted according to the given comparison callable
555
     * but does not maintain index association.
556
     *
557
     * This method MUST retain the state of the current instance, and return
558
     * an instance that contains the sorted intervals. The key are re-indexed
559
     */
560 24
    public function sorted(callable $compare): self
561
    {
562 24
        $intervals = $this->intervals;
563 24
        usort($intervals, $compare);
564 24
        if ($intervals === $this->intervals) {
565 12
            return $this;
566
        }
567
568 15
        return new self(...$intervals);
569
    }
570
571
    /**
572
     * Returns an instance where the given function is applied to each element in
573
     * the collection. The callable MUST return a Period object and takes a Period
574
     * and its associated key as argument.
575
     *
576
     * This method MUST retain the state of the current instance, and return
577
     * an instance that contains the returned intervals.
578
     */
579 9
    public function map(callable $func): self
580
    {
581 9
        $intervals = [];
582 9
        foreach ($this->intervals as $offset => $interval) {
583 9
            $intervals[$offset] = $func($interval, $offset);
584
        }
585
586 9
        if ($intervals === $this->intervals) {
587 3
            return $this;
588
        }
589
590 6
        $mapped = new self();
591 6
        $mapped->intervals = $intervals;
592
593 6
        return $mapped;
594
    }
595
596
    /**
597
     * Iteratively reduces the sequence to a single value using a callback.
598
     *
599
     * @param callable $func Accepts the carry, the current value and the current offset, and
600
     *                       returns an updated carry value.
601
     *
602
     * @param mixed|null $carry Optional initial carry value.
603
     *
604
     * @return mixed The carry value of the final iteration, or the initial
605
     *               value if the sequence was empty.
606
     */
607 15
    public function reduce(callable $func, $carry = null)
608
    {
609 15
        foreach ($this->intervals as $offset => $interval) {
610 15
            $carry = $func($carry, $interval, $offset);
611
        }
612
613 15
        return $carry;
614
    }
615
}
616