Completed
Push — master ( bd9527...dff488 )
by ignace nyamagana
05:23
created

Sequence::subtractOne()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 3
eloc 8
c 1
b 1
f 0
nc 2
nop 2
dl 0
loc 16
ccs 9
cts 9
cp 1
crap 3
rs 10
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 123
    public function __construct(Period ...$intervals)
54
    {
55 123
        $this->intervals = $intervals;
56 123
    }
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
     * DEPRECATION WARNING! This method will be removed in the next major point release.
183
     *
184
     * @deprecated since version 4.9.0
185
     * @see ::subtract
186
     */
187 15
    public function substract(Sequence $sequence): self
188
    {
189 15
        return $this->subtract($sequence);
190
    }
191
192
    /**
193
     * Subtract a Sequence from the current instance.
194
     *
195
     * This method MUST retain the state of the current instance, and return
196
     * an instance that contains substracted intervals.
197
     */
198 15
    public function subtract(Sequence $sequence): self
199
    {
200 15
        if ($this->isEmpty()) {
201 3
            return $this;
202
        }
203
204 15
        $new = $sequence->reduce([$this, 'subtractOne'], $this);
205 15
        if ($new->intervals === $this->intervals) {
206 6
            return $this;
207
        }
208
209 9
        return $new;
210
    }
211
212
    /**
213
     * Substract an Interval from a Sequence.
214
     */
215 12
    private function subtractOne(Sequence $sequence, Period $interval): self
216
    {
217 12
        if ($sequence->isEmpty()) {
218 3
            return $sequence;
219
        }
220
221
        $reducer = function (Sequence $sequence, Period $period) use ($interval) {
222 12
            $substract = $period->substract($interval);
0 ignored issues
show
Deprecated Code introduced by
The function League\Period\Period::substract() has been deprecated: since version 4.9.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

222
            $substract = /** @scrutinizer ignore-deprecated */ $period->substract($interval);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

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