Completed
Pull Request — master (#67)
by Andrey
14:41
created

Period::containsInterval()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 3
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com).
5
 *
6
 * @author  Ignace Nyamagana Butera <[email protected]>
7
 * @license https://github.com/thephpleague/period/blob/master/LICENSE (MIT License)
8
 * @version 4.0.0
9
 * @link    https://github.com/thephpleague/period
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
declare(strict_types=1);
16
17
namespace League\Period;
18
19
use DateInterval;
20
use DatePeriod;
21
use DateTimeImmutable;
22
use DateTimeInterface;
23
use DateTimeZone;
24
use JsonSerializable;
25
26
/**
27
 * A immutable value object class to manipulate Time interval.
28
 *
29
 * @package League.period
30
 * @author  Ignace Nyamagana Butera <[email protected]>
31
 * @since   1.0.0
32
 */
33
final class Period implements JsonSerializable
34
{
35
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
36
37
    /**
38
     * The starting included datepoint.
39
     *
40
     * @var DateTimeImmutable
41
     */
42
    private $startDate;
43
44
    /**
45
     * The ending excluded datepoint.
46
     *
47
     * @var DateTimeImmutable
48
     */
49
    private $endDate;
50
51 3
    /**
52
     * @inheritdoc
53 3
     */
54
    public static function __set_state(array $interval)
55
    {
56
        return new self($interval['startDate'], $interval['endDate']);
57
    }
58
59
    /**
60
     * Creates a new instance.
61
     *
62
     * @param mixed $startDate the starting included datepoint
63
     * @param mixed $endDate   the ending excluded datepoint
64 255
     *
65
     * @throws Exception If $startDate is greater than $endDate
66 255
     */
67 255
    public function __construct($startDate, $endDate)
68 255
    {
69 39
        $startDate = datepoint($startDate);
70
        $endDate = datepoint($endDate);
71 246
        if ($startDate > $endDate) {
72 246
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
73 246
        }
74
        $this->startDate = $startDate;
75
        $this->endDate = $endDate;
76
    }
77
78
    /**
79
     * Returns the starting included datepoint.
80
     */
81
    public function getStartDate(): DateTimeImmutable
82 282
    {
83
        return $this->startDate;
84 282
    }
85 246
86
    /**
87
     * Returns the ending excluded datepoint.
88 222
     */
89 78
    public function getEndDate(): DateTimeImmutable
90
    {
91
        return $this->endDate;
92 153
    }
93
94
    /**
95
     * Returns the instance duration as expressed in seconds.
96
     */
97
    public function getTimestampInterval(): float
98
    {
99
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
100
    }
101
102
    /**
103
     * Returns the instance duration as a DateInterval object.
104 6
     */
105
    public function getDateInterval(): DateInterval
106 6
    {
107 3
        return $this->startDate->diff($this->endDate);
108
    }
109
110 3
    /**
111
     * Allows iteration over a set of dates and times,
112
     * recurring at regular intervals, over the instance.
113
     *
114
     * @see http://php.net/manual/en/dateperiod.construct.php
115
     */
116
    public function getDatePeriod($duration, int $option = 0): DatePeriod
117
    {
118
        return new DatePeriod($this->startDate, duration($duration), $this->endDate, $option);
119
    }
120
121
    /**
122
     * Allows iteration over a set of dates and times,
123
     * recurring at regular intervals, over the instance backwards starting from
124
     * the instance ending datepoint.
125
     */
126
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
127
    {
128 138
        $duration = duration($duration);
129
        $date = $this->endDate;
130 138
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
131
            $date = $this->endDate->sub($duration);
132 138
        }
133
134
        while ($date > $this->startDate) {
135
            yield $date;
136
            $date = $date->sub($duration);
137
        }
138
    }
139
140
    /**
141
     * Returns the string representation as a ISO8601 interval format.
142
     *
143
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
144
     *
145
     * @return string
146
     */
147
    public function __toString()
148
    {
149 177
        $interval = $this->jsonSerialize();
150
151 177
        return $interval['startDate'].'/'.$interval['endDate'];
152 27
    }
153
154
    /**
155 165
     * Returns the JSON representation of an instance.
156 30
     *
157
     * Based on the JSON representation of dates as
158
     * returned by Javascript Date.toJSON() method.
159 153
     *
160
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
161
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
162
     *
163
     * @return array<string>
164
     */
165
    public function jsonSerialize()
166
    {
167
        $utc = new DateTimeZone('UTC');
168
169
        return [
170
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
171
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
172
        ];
173
    }
174
175
    /**
176
     * Returns the mathematical representation of an instance as a left close, right open interval.
177 24
     *
178
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
179 24
     * @see https://php.net/manual/en/function.date.php
180
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
181 24
     *
182
     * @param string $format the format of the outputted date string
183
     */
184
    public function format(string $format): string
185
    {
186
        return '['.$this->startDate->format($format).', '.$this->endDate->format($format).')';
187
    }
188
189
    /**
190
     * Compares two instances according to their duration.
191 18
     *
192
     * Returns:
193 18
     * <ul>
194 15
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
195
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
196 15
     * <li>  0 if both Interval objects have the same duration</li>
197
     * </ul>
198
     */
199 6
    public function durationCompare(self $interval): int
200
    {
201 6
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
202
    }
203
204
    /**
205
     * Tells whether the current instance duration is equal to the submitted one.
206
     */
207
    public function durationEquals(self $interval): bool
208
    {
209
        return 0 === $this->durationCompare($interval);
210
    }
211
212
    /**
213 21
     * Tells whether the current instance duration is greater than the submitted one.
214
     */
215 21
    public function durationGreaterThan(self $interval): bool
216
    {
217
        return 1 === $this->durationCompare($interval);
218
    }
219
220
    /**
221
     * Tells whether the current instance duration is less than the submitted one.
222
     */
223
    public function durationLessThan(self $interval): bool
224
    {
225
        return -1 === $this->durationCompare($interval);
226 12
    }
227
228 12
    /**
229 3
     * Tells whether two intervals share the same datepoints.
230 3
     *
231 3
     * [--------------------)
232
     * [--------------------)
233 3
     */
234
    public function equals(self $interval): bool
235
    {
236 12
        return $this->startDate == $interval->startDate
237 6
            && $this->endDate == $interval->endDate;
238
    }
239 6
240
    /**
241
     * Tells whether two intervals abuts.
242
     *
243
     * [--------------------)
244
     *                      [--------------------)
245
     * or
246
     *                      [--------------------)
247
     * [--------------------)
248
     */
249
    public function abuts(self $interval): bool
250
    {
251
        return $this->startDate == $interval->endDate
252
            || $this->endDate == $interval->startDate;
253 75
    }
254
255 75
    /**
256 75
     * Tells whether two intervals overlaps.
257 51
     *
258
     * [--------------------)
259
     *          [--------------------)
260 24
     */
261
    public function overlaps(self $interval): bool
262
    {
263
        return $this->startDate < $interval->endDate
264
            && $this->endDate > $interval->startDate;
265
    }
266
267
    /**
268
     * Tells whether an interval is entirely after the specified index.
269
     * The index can be a DateTimeInterface object or another Period object.
270
     *
271 12
     *                          [--------------------)
272
     * [--------------------)
273 12
     */
274 3
    public function isAfter($index): bool
275 3
    {
276 3
        if ($index instanceof self) {
277
            return $this->startDate >= $index->endDate;
278 3
        }
279
280
        return $this->startDate > datepoint($index);
281 12
    }
282 6
283
    /**
284 6
     * Tells whether an instance is entirely before the specified index.
285
     *
286
     * The index can be a DateTimeInterface object or another Period object.
287
     *
288
     * [--------------------)
289
     *                          [--------------------)
290
     */
291
    public function isBefore($index): bool
292
    {
293
        if ($index instanceof self) {
294
            return $this->endDate <= $index->startDate;
295 39
        }
296
297 39
        return $this->endDate <= datepoint($index);
298 6
    }
299
300 6
    /**
301
     * Tells whether an instance fully contains the specified index.
302
     *
303 36
     * The index can be a DateTimeInterface object or another Period object.
304
     *
305 30
     */
306
    public function contains($index): bool
307
    {
308
        if ($index instanceof self) {
309
            return $this->containsInterval($index);
310
        }
311
312
        return $this->containsDatePoint(datepoint($index));
313
    }
314
315
    /**
316 24
     * Tells whether an instance fully contains another instance.
317
     *
318 24
     * [--------------------)
319 3
     *     [----------)
320 3
     */
321 3
    private function containsInterval(self $interval): bool
322 3
    {
323
        return $this->containsDatePoint($interval->startDate)
324
            && ($interval->endDate >= $this->startDate && $interval->endDate <= $this->endDate);
325 3
    }
326
327
    /**
328 24
     * Tells whether an instance contains a datepoint.
329 24
     *
330 18
     * [------|------------)
331
     */
332
    private function containsDatePoint(DateTimeInterface $datepoint): bool
333 18
    {
334
        return $datepoint >= $this->startDate && $datepoint < $this->endDate;
335
    }
336
337
    /**
338
     * Allows splitting an instance in smaller Period objects according to a given interval.
339
     *
340
     * The returned iterable Interval set is ordered so that:
341
     * <ul>
342
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
343
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
344
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
345
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
346 3
     * </ul>
347
     *
348 3
     * @return iterable<Period>
0 ignored issues
show
Documentation introduced by
The doc-type iterable<Period> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
349
     */
350 3
    public function split($duration): iterable
351
    {
352
        $startDate = $this->startDate;
353
        $duration = duration($duration);
354
        do {
355
            $endDate = $startDate->add($duration);
356
            if ($endDate > $this->endDate) {
357
                $endDate = $this->endDate;
358
            }
359
            yield new self($startDate, $endDate);
360
361
            $startDate = $endDate;
362
        } while ($startDate < $this->endDate);
363 3
    }
364
365 3
    /**
366
     * Allows splitting an instance in smaller Period objects according to a given interval.
367 3
     *
368
     * The returned iterable Period set is ordered so that:
369
     * <ul>
370
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
371
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
372
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
373
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
374
     * </ul>
375
     *
376
     * @return iterable<Period>
0 ignored issues
show
Documentation introduced by
The doc-type iterable<Period> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
377
     */
378
    public function splitBackwards($duration): iterable
379
    {
380 3
        $endDate = $this->endDate;
381
        $duration = duration($duration);
382 3
        do {
383
            $startDate = $endDate->sub($duration);
384 3
            if ($startDate < $this->startDate) {
385
                $startDate = $this->startDate;
386
            }
387
            yield new self($startDate, $endDate);
388
389
            $endDate = $startDate;
390
        } while ($endDate > $this->startDate);
391
    }
392
393
    /**
394
     * Returns the computed intersection between two instances as a new instance.
395
     *
396
     * [--------------------)
397 3
     *          ∩
398
     *                 [----------)
399 3
     *          =
400
     *                 [----)
401 3
     *
402
     * @throws Exception If both objects do not overlaps
403
     */
404
    public function intersect(self $interval): self
405
    {
406
        if (!$this->overlaps($interval)) {
407
            throw new Exception('Both '.self::class.' objects should overlaps');
408
        }
409
410
        return new self(
411
            ($interval->startDate > $this->startDate) ? $interval->startDate : $this->startDate,
412 177
            ($interval->endDate < $this->endDate) ? $interval->endDate : $this->endDate
413
        );
414 177
    }
415
416
    /**
417
     * Returns the computed difference between two overlapping instances as
418
     * an array containing Period objects or the null value.
419
     *
420
     * The array will always contains 2 elements:
421
     *
422
     * <ul>
423
     * <li>an NULL filled array if both objects have the same datepoints</li>
424
     * <li>one Period object and NULL if both objects share one datepoint</li>
425 162
     * <li>two Period objects if both objects share no datepoint</li>
426
     * </ul>
427 162
     *
428
     * [--------------------)
429
     *          -
430
     *                [-----------)
431
     *          =
432
     * [--------------)  +  [-----)
433
     *
434
     * @return array<null|Period>
435 18
     */
436
    public function diff(self $interval): array
437 18
    {
438
        if ($interval->equals($this)) {
439
            return [null, null];
440
        }
441
442
        $intersect = $this->intersect($interval);
443
        $merge = $this->merge($interval);
444
        if ($merge->startDate == $intersect->startDate) {
445 33
            return [$merge->startingOn($intersect->endDate), null];
446
        }
447 33
448
        if ($merge->endDate == $intersect->endDate) {
449
            return [$merge->endingOn($intersect->startDate), null];
450
        }
451
452
        return [
453
            $merge->endingOn($intersect->startDate),
454
            $merge->startingOn($intersect->endDate),
455
        ];
456
    }
457
458
    /**
459
     * Returns the computed gap between two instances as a new instance.
460
     *
461
     * [--------------------)
462
     *          +
463
     *                          [----------)
464
     *          =
465
     *                      [---)
466
     *
467
     * @throws Exception If both instance overlaps
468
     */
469 24
    public function gap(self $interval): self
470
    {
471 24
        if ($this->overlaps($interval)) {
472
            throw new Exception('Both '.self::class.' objects must not overlaps');
473
        }
474
475
        if ($interval->startDate > $this->startDate) {
476
            return new self($this->endDate, $interval->startDate);
477
        }
478
479
        return new self($interval->endDate, $this->startDate);
480
    }
481
482
    /**
483
     * Returns the difference between two instances expressed in seconds.
484
     */
485
    public function timestampIntervalDiff(self $interval): float
486
    {
487
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
488
    }
489
490
    /**
491
     * Returns the difference between two instances expressed with a DateInterval object.
492
     */
493
    public function dateIntervalDiff(self $interval): DateInterval
494
    {
495
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
496
    }
497 12
498
    /**
499 12
     * Returns an instance with the specified starting datepoint.
500 12
     *
501
     * This method MUST retain the state of the current instance, and return
502 12
     * an instance that contains the specified starting datepoint.
503 12
     */
504 6
    public function startingOn($datepoint): self
505
    {
506 12
        $startDate = datepoint($datepoint);
507
        if ($startDate == $this->startDate) {
508 9
            return $this;
509 9
        }
510 9
511
        return new self($startDate, $this->endDate);
512
    }
513
514
    /**
515
     * Returns an instance with the specified ending datepoint.
516
     *
517
     * This method MUST retain the state of the current instance, and return
518
     * an instance that contains the specified ending datepoint.
519
     */
520
    public function endingOn($datepoint): self
521
    {
522
        $endDate = datepoint($datepoint);
523
        if ($endDate == $this->endDate) {
524
            return $this;
525
        }
526
527
        return new self($this->startDate, $endDate);
528
    }
529
530
    /**
531
     * Returns a new instance with a new ending datepoint.
532
     *
533
     * This method MUST retain the state of the current instance, and return
534
     * an instance that contains the specified ending datepoint.
535 6
     */
536
    public function withDurationAfterStart($duration): self
537 6
    {
538 6
        return $this->endingOn($this->startDate->add(duration($duration)));
539
    }
540 6
541 6
    /**
542 3
     * Returns a new instance with a new starting datepoint.
543
     *
544 6
     * This method MUST retain the state of the current instance, and return
545
     * an instance that contains the specified starting datepoint.
546 6
     */
547 6
    public function withDurationBeforeEnd($duration): self
548 6
    {
549
        return $this->startingOn($this->endDate->sub(duration($duration)));
550
    }
551
552
    /**
553
     * Returns a new instance with a new starting datepoint
554
     * moved forward or backward by the given interval.
555
     *
556
     * This method MUST retain the state of the current instance, and return
557
     * an instance that contains the specified starting datepoint.
558 3
     */
559
    public function moveStartDate($duration): self
560 3
    {
561
        return $this->startingOn($this->startDate->add(duration($duration)));
562 3
    }
563
564
    /**
565
     * Returns a new instance with a new ending datepoint
566
     * moved forward or backward by the given interval.
567
     *
568
     * This method MUST retain the state of the current instance, and return
569
     * an instance that contains the specified ending datepoint.
570
     */
571
    public function moveEndDate($duration): self
572
    {
573 6
        return $this->endingOn($this->endDate->add(duration($duration)));
574
    }
575 6
576 6
    /**
577 6
     * Returns a new instance where the datepoints
578
     * are moved forwards or backward simultaneously by the given DateInterval.
579
     *
580 6
     * This method MUST retain the state of the current instance, and return
581 6
     * an instance that contains the specified new datepoints.
582
     */
583
    public function move($duration): self
584
    {
585
        $duration = duration($duration);
586
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration));
587
        if ($this->equals($interval)) {
588
            return $this;
589
        }
590
591
        return $interval;
592
    }
593
594
    /**
595
     * Returns an instance where the given DateInterval is simultaneously
596
     * substracted from the starting datepoint and added to the ending datepoint.
597
     *
598
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
599 21
     *
600
     * This method MUST retain the state of the current instance, and return
601 21
     * an instance that contains the specified new datepoints.
602
     */
603
    public function expand($duration): self
604
    {
605
        $duration = duration($duration);
606
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration));
607
        if ($this->equals($interval)) {
608
            return $this;
609
        }
610
611
        return $interval;
612 9
    }
613
614 9
    /**
615
     * Merges one or more instances to return a new instance.
616
     * The resulting instance represents the largest duration possible.
617
     *
618
     * This method MUST retain the state of the current instance, and return
619
     * an instance that contains the specified new datepoints.
620
     *
621
     * @param Period ...$intervals
622
     */
623
    public function merge(self $interval, self ...$intervals): self
624
    {
625 9
        $intervals[] = $interval;
626
        $carry = $this;
627 9
        foreach ($intervals as $interval) {
628
            if ($carry->startDate > $interval->startDate) {
629
                $carry = $carry->startingOn($interval->startDate);
630
            }
631
632
            if ($carry->endDate < $interval->endDate) {
633
                $carry = $carry->endingOn($interval->endDate);
634
            }
635
        }
636
637
        return $carry;
638 3
    }
639
}
640