Completed
Push — master ( 037af6...82c71b )
by ignace nyamagana
03:30
created

Sequence::offsetGet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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