Sequence   F
last analyzed

Complexity

Total Complexity 86

Size/Duplication

Total Lines 642
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 17
Bugs 5 Features 0
Metric Value
wmc 86
eloc 162
c 17
b 5
f 0
dl 0
loc 642
ccs 209
cts 209
cp 1
rs 2

40 Methods

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