Completed
Push — master ( e4aca7...8634af )
by ignace nyamagana
03:46
created

Sequence   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 615
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 156
dl 0
loc 615
ccs 202
cts 202
cp 1
rs 2
c 0
b 0
f 0
wmc 84

39 Methods

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