Completed
Push — master ( 31d1b5...b881c5 )
by ignace nyamagana
02:12
created

Sequence::offsetSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
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
    public function __construct(Period ...$intervals)
54
    {
55
        $this->intervals = $intervals;
56
    }
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
    public function getBoundaries(): ?Period
66
    {
67
        $period = reset($this->intervals);
68
        if (false === $period) {
69
            return null;
70
        }
71
72
        return $period->merge(...$this->intervals);
73
    }
74
75
    /**
76
     * Returns the gaps inside the instance.
77
     */
78
    public function getGaps(): self
79
    {
80
        $sequence = new self();
81
        $interval = null;
82
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
83
            if (null === $interval) {
84
                $interval = $period;
85
                continue;
86
            }
87
88
            if (!$interval->overlaps($period) && !$interval->abuts($period)) {
89
                $sequence->push($interval->gap($period));
90
            }
91
92
            if (!$interval->contains($period)) {
93
                $interval = $period;
94
            }
95
        }
96
97
        return $sequence;
98
    }
99
100
    /**
101
     * Sorts two Interval instance using their start datepoint.
102
     */
103
    private function sortByStartDate(Period $interval1, Period $interval2): int
104
    {
105
        return $interval1->getStartDate() <=> $interval2->getStartDate();
106
    }
107
108
    /**
109
     * Returns the intersections inside the instance.
110
     */
111
    public function getIntersections(): self
112
    {
113
        $sequence = new self();
114
        $current = null;
115
        $isPreviouslyContained = false;
116
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
117
            if (null === $current) {
118
                $current = $period;
119
                continue;
120
            }
121
122
            $isContained = $current->contains($period);
123
            if ($isContained && $isPreviouslyContained) {
124
                continue;
125
            }
126
127
            if ($current->overlaps($period)) {
128
                $sequence->push($current->intersect($period));
129
            }
130
131
            $isPreviouslyContained = $isContained;
132
            if (!$isContained) {
133
                $current = $period;
134
            }
135
        }
136
137
        return $sequence;
138
    }
139
140
    /**
141
     * Tells whether some intervals in the current instance satisfies the predicate.
142
     */
143
    public function some(callable $predicate): bool
144
    {
145
        foreach ($this->intervals as $offset => $interval) {
146
            if (true === $predicate($interval, $offset)) {
147
                return true;
148
            }
149
        }
150
151
        return false;
152
    }
153
154
    /**
155
     * Tells whether all intervals in the current instance satisfies the predicate.
156
     */
157
    public function every(callable $predicate): bool
158
    {
159
        foreach ($this->intervals as $offset => $interval) {
160
            if (true !== $predicate($interval, $offset)) {
161
                return false;
162
            }
163
        }
164
165
        return [] !== $this->intervals;
166
    }
167
168
    /**
169
     * Returns the array representation of the sequence.
170
     *
171
     * @return Period[]
172
     */
173
    public function toArray(): array
174
    {
175
        return $this->intervals;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function jsonSerialize(): array
182
    {
183
        return $this->intervals;
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189
    public function getIterator(): Iterator
190
    {
191
        foreach ($this->intervals as $offset => $interval) {
192
            yield $offset => $interval;
193
        }
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function count(): int
200
    {
201
        return count($this->intervals);
202
    }
203
204
    /**
205
     * @inheritdoc
206
     *
207
     * @param int $offset
208
     */
209
    public function offsetExists($offset): bool
210
    {
211
        return isset($this->intervals[$offset]);
212
    }
213
214
    /**
215
     * @inheritdoc
216
     *
217
     * @param int $offset
218
     */
219
    public function offsetGet($offset): Period
220
    {
221
        return $this->get($offset);
222
    }
223
224
    /**
225
     * @inheritdoc
226
     *
227
     * @param int $offset
228
     */
229
    public function offsetUnset($offset): void
230
    {
231
        $this->remove($offset);
232
    }
233
234
    /**
235
     * @inheritdoc
236
     *
237
     * @param mixed|int $offset
238
     * @param Period    $interval
239
     */
240
    public function offsetSet($offset, $interval): void
241
    {
242
        if (null !== $offset) {
243
            $this->set($offset, $interval);
244
            return;
245
        }
246
247
        $this->push($interval);
248
    }
249
250
    /**
251
     * Tells whether the sequence is empty.
252
     */
253
    public function isEmpty(): bool
254
    {
255
        return [] === $this->intervals;
256
    }
257
258
    /**
259
     * Tells whether the given interval is present in the sequence.
260
     *
261
     * @param Period ...$intervals
262
     */
263
    public function contains(Period $interval, Period ...$intervals): bool
264
    {
265
        $intervals[] = $interval;
266
        foreach ($intervals as $period) {
267
            if (false === $this->indexOf($period)) {
268
                return false;
269
            }
270
        }
271
272
        return true;
273
    }
274
275
    /**
276
     * Attempts to find the first offset attached to the submitted interval.
277
     *
278
     * If no offset is found the method returns boolean false.
279
     *
280
     * @return int|bool
281
     */
282
    public function indexOf(Period $interval)
283
    {
284
        foreach ($this->intervals as $offset => $period) {
285
            if ($period->equals($interval)) {
286
                return $offset;
287
            }
288
        }
289
290
        return false;
291
    }
292
293
    /**
294
     * Returns the interval specified at a given offset.
295
     *
296
     * @throws InvalidIndex If the offset is illegal for the current sequence
297
     */
298
    public function get(int $offset): Period
299
    {
300
        $period = $this->intervals[$offset] ?? null;
301
        if (null !== $period) {
302
            return $period;
303
        }
304
305
        throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
306
    }
307
308
    /**
309
     * Sort the current instance according to the given comparison callable
310
     * and maintain index association.
311
     *
312
     * Returns true on success or false on failure
313
     */
314
    public function sort(callable $compare): bool
315
    {
316
        return uasort($this->intervals, $compare);
317
    }
318
319
    /**
320
     * Adds new intervals at the front of the sequence.
321
     *
322
     * The sequence is re-indexed after addition
323
     *
324
     * @param Period ...$intervals
325
     */
326
    public function unshift(Period $interval, Period ...$intervals): void
327
    {
328
        $this->intervals = array_merge([$interval], $intervals, $this->intervals);
329
    }
330
331
    /**
332
     * Adds new intervals at the end of the sequence.
333
     *
334
     * @param Period ...$intervals
335
     */
336
    public function push(Period $interval, Period ...$intervals): void
337
    {
338
        $this->intervals = array_merge($this->intervals, [$interval], $intervals);
339
    }
340
341
    /**
342
     * Inserts new intervals at the specified offset of the sequence.
343
     *
344
     * The sequence is re-indexed after addition
345
     *
346
     * @param Period ...$intervals
347
     *
348
     * @throws InvalidIndex If the offset is illegal for the current sequence.
349
     */
350
    public function insert(int $offset, Period $interval, Period ...$intervals): void
351
    {
352
        if ($offset < 0 || $offset > count($this->intervals)) {
353
            throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
354
        }
355
356
        array_unshift($intervals, $interval);
357
        array_splice($this->intervals, $offset, 0, $intervals);
358
    }
359
360
    /**
361
     * Updates the interval at the specify offset.
362
     *
363
     * @throws InvalidIndex If the offset is illegal for the current sequence.
364
     */
365
    public function set(int $offset, Period $interval): void
366
    {
367
        $this->get($offset);
368
        $this->intervals[$offset] = $interval;
369
    }
370
371
    /**
372
     * Removes an interval from the sequence at the given offset and returns it.
373
     *
374
     * The sequence is re-indexed after removal
375
     *
376
     * @throws InvalidIndex If the offset is illegal for the current sequence.
377
     */
378
    public function remove(int $offset): Period
379
    {
380
        $interval = $this->get($offset);
381
        unset($this->intervals[$offset]);
382
383
        $this->intervals = array_values($this->intervals);
384
385
        return $interval;
386
    }
387
388
    /**
389
     * Filters the sequence according to the given predicate.
390
     *
391
     * This method MUST retain the state of the current instance, and return
392
     * an instance that contains the interval which validate the predicate.
393
     */
394
    public function filter(callable $predicate): self
395
    {
396
        $intervals = array_filter($this->intervals, $predicate, ARRAY_FILTER_USE_BOTH);
397
        if ($intervals === $this->intervals) {
398
            return $this;
399
        }
400
401
        return new self(...$intervals);
402
    }
403
404
    /**
405
     * Removes all intervals from the sequence.
406
     */
407
    public function clear(): void
408
    {
409
        $this->intervals = [];
410
    }
411
412
    /**
413
     * Returns an instance sorted according to the given comparison callable
414
     * but does not maintain index association.
415
     *
416
     * This method MUST retain the state of the current instance, and return
417
     * an instance that contains the sorted intervals. The key are re-indexed
418
     */
419
    public function sorted(callable $compare): self
420
    {
421
        $intervals = $this->intervals;
422
        usort($intervals, $compare);
423
        if ($intervals === $this->intervals) {
424
            return $this;
425
        }
426
427
        return new self(...$intervals);
428
    }
429
430
    /**
431
     * Returns an instance where the given function is applied to each element in
432
     * the collection. The callable MUST return a Period object and takes a Period
433
     * and its associated key as argument.
434
     *
435
     * This method MUST retain the state of the current instance, and return
436
     * an instance that contains the returned intervals.
437
     */
438
    public function map(callable $func): self
439
    {
440
        $intervals = [];
441
        foreach ($this->intervals as $offset => $interval) {
442
            $intervals[$offset] = $func($interval, $offset);
443
        }
444
445
        if ($intervals === $this->intervals) {
446
            return $this;
447
        }
448
449
        $mapped = new self();
450
        $mapped->intervals = $intervals;
451
452
        return $mapped;
453
    }
454
}
455