Passed
Push — master ( e3542e...0b478a )
by ignace nyamagana
01:45
created

Sequence::diff()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 15
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 Countable;
17
use Iterator;
18
use IteratorAggregate;
19
use JsonSerializable;
20
use function array_filter;
21
use function array_merge;
22
use function count;
23
use function reset;
24
use function sort;
25
use function sprintf;
26
use function uasort;
27
use const ARRAY_FILTER_USE_BOTH;
28
29
/**
30
 * A class to manipulate interval collection.
31
 *
32
 * @package League.period
33
 * @author  Ignace Nyamagana Butera <[email protected]>
34
 * @since   4.1.0
35
 */
36
final class Sequence implements Countable, IteratorAggregate, JsonSerializable
37
{
38
    /**
39
     * @var Period[]
40
     */
41
    private $intervals = [];
42
43
    /**
44
     * new instance.
45
     *
46
     * @param Period ...$intervals
47
     */
48
    public function __construct(Period ...$intervals)
49
    {
50
        $this->intervals = $intervals;
51
    }
52
53
    /**
54
     * Returns the sequence boundaries as a Period instance.
55
     *
56
     * If the sequence contains no interval null is returned.
57
     *
58
     * @return ?Period
59
     */
60
    public function getBoundaries(): ?Period
61
    {
62
        $period = reset($this->intervals);
63
        if (false === $period) {
64
            return null;
65
        }
66
67
        return $period->merge(...$this->intervals);
68
    }
69
70
    /**
71
     * Returns the gaps inside the instance.
72
     */
73
    public function getGaps(): self
74
    {
75
        $sequence = new self();
76
        $interval = null;
77
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
78
            if (null === $interval) {
79
                $interval = $period;
80
                continue;
81
            }
82
83
            if (!$interval->overlaps($period) && !$interval->abuts($period)) {
84
                $sequence->push($interval->gap($period));
85
            }
86
87
            if (!$interval->contains($period)) {
88
                $interval = $period;
89
            }
90
        }
91
92
        return $sequence;
93
    }
94
95
    /**
96
     * Sorts two Interval instance using their start datepoint.
97
     */
98
    private function sortByStartDate(Period $interval1, Period $interval2): int
99
    {
100
        return $interval1->getStartDate() <=> $interval2->getStartDate();
101
    }
102
103
    /**
104
     * Returns the intersections inside the instance.
105
     */
106
    public function getIntersections(): self
107
    {
108
        $sequence = new self();
109
        $current = null;
110
        $isPreviouslyContained = false;
111
        foreach ($this->sorted([$this, 'sortByStartDate']) as $period) {
112
            if (null === $current) {
113
                $current = $period;
114
                continue;
115
            }
116
117
            $isContained = $current->contains($period);
118
            if ($isContained && $isPreviouslyContained) {
119
                continue;
120
            }
121
122
            if ($current->overlaps($period)) {
123
                $sequence->push($current->intersect($period));
124
            }
125
126
            $isPreviouslyContained = $isContained;
127
            if (!$isContained) {
128
                $current = $period;
129
            }
130
        }
131
132
        return $sequence;
133
    }
134
135
    /**
136
     * Returns the computed difference between a sequence and an interval.
137
     */
138
    public function diff(Period $interval): self
139
    {
140
        $predicate = static function (Period $period) use ($interval): bool {
141
            return $interval->overlaps($period);
142
        };
143
144
        $sequence = $this->filter($predicate);
145
        if ($sequence->isEmpty()) {
146
            return $sequence;
147
        }
148
149
        $sequence->push(new Period($interval->getStartDate(), $interval->getStartDate()));
150
        $sequence->push(new Period($interval->getEndDate(), $interval->getEndDate()));
151
152
        return $sequence->getGaps();
153
    }
154
155
    /**
156
     * Tells whether some intervals in the current instance satisfies the predicate.
157
     */
158
    public function some(callable $predicate): bool
159
    {
160
        foreach ($this->intervals as $offset => $interval) {
161
            if (true === $predicate($interval, $offset)) {
162
                return true;
163
            }
164
        }
165
166
        return false;
167
    }
168
169
    /**
170
     * Tells whether all intervals in the current instance satisfies the predicate.
171
     */
172
    public function every(callable $predicate): bool
173
    {
174
        foreach ($this->intervals as $offset => $interval) {
175
            if (true !== $predicate($interval, $offset)) {
176
                return false;
177
            }
178
        }
179
180
        return [] !== $this->intervals;
181
    }
182
183
    /**
184
     * Returns the array representation of the sequence.
185
     *
186
     * @return Period[]
187
     */
188
    public function toArray(): array
189
    {
190
        return $this->intervals;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196
    public function jsonSerialize(): array
197
    {
198
        return $this->intervals;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function getIterator(): Iterator
205
    {
206
        foreach ($this->intervals as $offset => $interval) {
207
            yield $offset => $interval;
208
        }
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function count(): int
215
    {
216
        return count($this->intervals);
217
    }
218
219
    /**
220
     * Tells whether the sequence is empty.
221
     */
222
    public function isEmpty(): bool
223
    {
224
        return [] === $this->intervals;
225
    }
226
227
    /**
228
     * Tells whether the given interval is present in the sequence.
229
     *
230
     * @param Period ...$intervals
231
     */
232
    public function contains(Period $interval, Period ... $intervals): bool
233
    {
234
        $intervals[] = $interval;
235
        foreach ($intervals as $period) {
236
            if (null === $this->indexOf($period)) {
237
                return false;
238
            }
239
        }
240
241
        return true;
242
    }
243
244
    /**
245
     * Attempts to find the first offset attached to the submitted interval.
246
     *
247
     * If no offset is found the method returns null.
248
     *
249
     * @return ?int
250
     */
251
    public function indexOf(Period $interval): ?int
252
    {
253
        foreach ($this->intervals as $offset => $period) {
254
            if ($period->equals($interval)) {
255
                return $offset;
256
            }
257
        }
258
259
        return null;
260
    }
261
262
    /**
263
     * Returns the interval specified at a given offset.
264
     *
265
     * @throws InvalidIndex If the offset is illegal for the current sequence
266
     */
267
    public function get(int $offset): Period
268
    {
269
        $period = $this->intervals[$offset] ?? null;
270
        if (null !== $period) {
271
            return $period;
272
        }
273
274
        throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
275
    }
276
277
    /**
278
     * Sort the current instance according to the given comparison callable
279
     * and maintain index association.
280
     *
281
     * Returns true on success or false on failure
282
     */
283
    public function sort(callable $compare): bool
284
    {
285
        return uasort($this->intervals, $compare);
286
    }
287
288
    /**
289
     * Adds new intervals at the front of the sequence.
290
     *
291
     * The sequence is re-indexed after addition
292
     *
293
     * @param Period ...$intervals
294
     */
295
    public function unshift(Period $interval, Period ...$intervals): void
296
    {
297
        $this->intervals = array_merge([$interval], $intervals, $this->intervals);
298
    }
299
300
    /**
301
     * Adds new intervals at the end of the sequence.
302
     *
303
     * @param Period ...$intervals
304
     */
305
    public function push(Period $interval, Period ...$intervals): void
306
    {
307
        $this->intervals = array_merge($this->intervals, [$interval], $intervals);
308
    }
309
310
    /**
311
     * Inserts new intervals at the specified offset of the sequence.
312
     *
313
     * The sequence is re-indexed after addition
314
     *
315
     * @param Period ...$intervals
316
     *
317
     * @throws InvalidIndex If the offset is illegal for the current sequence.
318
     */
319
    public function insert(int $offset, Period $interval, Period ...$intervals): void
320
    {
321
        if ($offset < 0 || $offset > count($this->intervals)) {
322
            throw new InvalidIndex(sprintf('%s is an invalid offset in the current sequence', $offset));
323
        }
324
325
        array_unshift($intervals, $interval);
326
        array_splice($this->intervals, $offset, 0, $intervals);
327
    }
328
329
    /**
330
     * Updates the interval at the specify offset.
331
     *
332
     * @throws InvalidIndex If the offset is illegal for the current sequence.
333
     */
334
    public function set(int $offset, Period $interval): void
335
    {
336
        $this->get($offset);
337
        $this->intervals[$offset] = $interval;
338
    }
339
340
    /**
341
     * Removes an interval from the sequence at the given offset and returns it.
342
     *
343
     * The sequence is re-indexed after removal
344
     *
345
     * @throws InvalidIndex If the offset is illegal for the current sequence.
346
     */
347
    public function remove(int $offset): Period
348
    {
349
        $interval = $this->get($offset);
350
        unset($this->intervals[$offset]);
351
352
        $this->intervals = array_values($this->intervals);
353
354
        return $interval;
355
    }
356
357
    /**
358
     * Filters the sequence according to the given predicate.
359
     *
360
     * This method MUST retain the state of the current instance, and return
361
     * an instance that contains the interval which validate the predicate.
362
     */
363
    public function filter(callable $predicate): self
364
    {
365
        $intervals = array_filter($this->intervals, $predicate, ARRAY_FILTER_USE_BOTH);
366
        if ($intervals === $this->intervals) {
367
            return $this;
368
        }
369
370
        return new self(...$intervals);
371
    }
372
373
    /**
374
     * Removes all intervals from the sequence.
375
     */
376
    public function clear(): void
377
    {
378
        $this->intervals = [];
379
    }
380
381
    /**
382
     * Returns an instance sorted according to the given comparison callable
383
     * but does not maintain index association.
384
     *
385
     * This method MUST retain the state of the current instance, and return
386
     * an instance that contains the sorted intervals. The key are re-indexed
387
     */
388
    public function sorted(callable $compare): self
389
    {
390
        $intervals = $this->intervals;
391
        usort($intervals, $compare);
392
        if ($intervals === $this->intervals) {
393
            return $this;
394
        }
395
396
        return new self(...$intervals);
397
    }
398
}
399