Completed
Pull Request — master (#93)
by ignace nyamagana
03:00
created

Period::expand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 4
b 0
f 0
nc 2
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
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 DateInterval;
17
use DatePeriod;
18
use DateTimeImmutable;
19
use DateTimeInterface;
20
use DateTimeZone;
21
use JsonSerializable;
22
use function array_filter;
23
use function array_keys;
24
use function implode;
25
use function sprintf;
26
use function str_replace;
27
use function strlen;
28
use function substr;
29
30
/**
31
 * A immutable value object class to manipulate Time interval.
32
 *
33
 * @package League.period
34
 * @author  Ignace Nyamagana Butera <[email protected]>
35
 * @since   1.0.0
36
 */
37
final class Period implements JsonSerializable
38
{
39
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
40
41
    private const BOUNDARY_TYPE = [
42
        self::INCLUDE_START_EXCLUDE_END => 1,
43
        self::INCLUDE_ALL => 1,
44
        self::EXCLUDE_START_INCLUDE_END => 1,
45
        self::EXCLUDE_ALL => 1,
46
    ];
47
48
    public const INCLUDE_START_EXCLUDE_END = '[)';
49
50
    public const EXCLUDE_START_INCLUDE_END = '(]';
51
52
    public const EXCLUDE_ALL = '()';
53
54
    public const INCLUDE_ALL = '[]';
55
56
    /**
57
     * The starting datepoint.
58
     *
59
     * @var DateTimeImmutable
60
     */
61
    private $startDate;
62
63
    /**
64
     * The ending datepoint.
65
     *
66
     * @var DateTimeImmutable
67
     */
68
    private $endDate;
69
70
    /**
71
     * The boundary type.
72
     *
73
     * @var string
74
     */
75
    private $boundaryType;
76
77
    /**
78
     * Creates a new instance.
79
     *
80
     * @param mixed $startDate the starting datepoint
81
     * @param mixed $endDate   the ending datepoint
82
     *
83
     * @throws Exception If $startDate is greater than $endDate
84
     */
85 897
    public function __construct($startDate, $endDate, string $boundaryType = self::INCLUDE_START_EXCLUDE_END)
86
    {
87 897
        $startDate = self::getDatepoint($startDate);
88 897
        $endDate = self::getDatepoint($endDate);
89 885
        if ($startDate > $endDate) {
90 66
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
91
        }
92
93 861
        if (!isset(self::BOUNDARY_TYPE[$boundaryType])) {
94 6
            throw new Exception(sprintf(
95 6
                'The boundary type `%s` is invalid. The only valid values are %s',
96 6
                $boundaryType,
97 6
                '`'.implode('`, `', array_keys(self::BOUNDARY_TYPE)).'`'
98
            ));
99
        }
100
101 858
        $this->startDate = $startDate;
102 858
        $this->endDate = $endDate;
103 858
        $this->boundaryType = $boundaryType;
104 858
    }
105
106
    /**
107
     * Returns a DateTimeImmutable instance.
108
     *
109
     * @param mixed $datepoint a Datepoint
110
     */
111 1062
    private static function getDatepoint($datepoint): DateTimeImmutable
112
    {
113 1062
        if ($datepoint instanceof DateTimeImmutable) {
114 840
            return $datepoint;
115
        }
116
117 720
        return Datepoint::create($datepoint);
118
    }
119
120
    /**
121
     * Returns a DateInterval instance.
122
     *
123
     * @param mixed $duration a Duration
124
     */
125 357
    private static function getDuration($duration): DateInterval
126
    {
127 357
        if ($duration instanceof DateInterval) {
128 150
            return $duration;
129
        }
130
131 207
        return Duration::create($duration);
132
    }
133
134
    /**************************************************
135
     * Named constructors
136
     **************************************************/
137
138
    /**
139
     * @inheritDoc
140
     */
141 6
    public static function __set_state(array $interval)
142
    {
143 6
        return new self($interval['startDate'], $interval['endDate'], $interval['boundaryType'] ?? self::INCLUDE_START_EXCLUDE_END);
144
    }
145
146
    /**
147
     * Creates new instance from a starting datepoint and a duration.
148
     *
149
     * @param mixed $startDate the starting datepoint
150
     * @param mixed $duration  a Duration
151
     */
152 132
    public static function after($startDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
153
    {
154 132
        $startDate = self::getDatepoint($startDate);
155
156 132
        return new self($startDate, $startDate->add(self::getDuration($duration)), $boundaryType);
157
    }
158
159
    /**
160
     * Creates new instance from a ending datepoint and a duration.
161
     *
162
     * @param mixed $endDate  the ending datepoint
163
     * @param mixed $duration a Duration
164
     */
165 27
    public static function before($endDate, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
166
    {
167 27
        $endDate = self::getDatepoint($endDate);
168
169 27
        return new self($endDate->sub(self::getDuration($duration)), $endDate, $boundaryType);
170
    }
171
172
    /**
173
     * Creates new instance where the given duration is simultaneously
174
     * subtracted from and added to the datepoint.
175
     *
176
     * @param mixed $datepoint a Datepoint
177
     * @param mixed $duration  a Duration
178
     */
179 21
    public static function around($datepoint, $duration, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
180
    {
181 21
        $datepoint = self::getDatepoint($datepoint);
182 21
        $duration = self::getDuration($duration);
183
184 21
        return new self($datepoint->sub($duration), $datepoint->add($duration), $boundaryType);
185
    }
186
187
    /**
188
     * Creates new instance from a DatePeriod.
189
     */
190 12
    public static function fromDatePeriod(DatePeriod $datePeriod, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
191
    {
192 12
        return new self($datePeriod->getStartDate(), $datePeriod->getEndDate(), $boundaryType);
193
    }
194
195
    /**
196
     * Creates new instance for a specific year.
197
     */
198 12
    public static function fromYear(int $year, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
199
    {
200 12
        $startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0);
201
202 12
        return new self($startDate, $startDate->add(new DateInterval('P1Y')), $boundaryType);
203
    }
204
205
    /**
206
     * Creates new instance for a specific ISO year.
207
     */
208 6
    public static function fromIsoYear(int $year, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
209
    {
210 6
        return new self(
211 6
            (new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0),
212 6
            (new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0),
213 4
            $boundaryType
214
        );
215
    }
216
217
    /**
218
     * Creates new instance for a specific year and semester.
219
     */
220 18
    public static function fromSemester(int $year, int $semester = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
221
    {
222 18
        $month = (($semester - 1) * 6) + 1;
223 18
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
224
225 18
        return new self($startDate, $startDate->add(new DateInterval('P6M')), $boundaryType);
226
    }
227
228
    /**
229
     * Creates new instance for a specific year and quarter.
230
     */
231 18
    public static function fromQuarter(int $year, int $quarter = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
232
    {
233 18
        $month = (($quarter - 1) * 3) + 1;
234 18
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
235
236 18
        return new self($startDate, $startDate->add(new DateInterval('P3M')), $boundaryType);
237
    }
238
239
    /**
240
     * Creates new instance for a specific year and month.
241
     */
242 75
    public static function fromMonth(int $year, int $month = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
243
    {
244 75
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
245
246 75
        return new self($startDate, $startDate->add(new DateInterval('P1M')), $boundaryType);
247
    }
248
249
    /**
250
     * Creates new instance for a specific ISO8601 week.
251
     */
252 21
    public static function fromIsoWeek(int $year, int $week = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
253
    {
254 21
        $startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0);
255
256 21
        return new self($startDate, $startDate->add(new DateInterval('P7D')), $boundaryType);
257
    }
258
259
    /**
260
     * Creates new instance for a specific year, month and day.
261
     */
262 51
    public static function fromDay(int $year, int $month = 1, int $day = 1, string $boundaryType = self::INCLUDE_START_EXCLUDE_END): self
263
    {
264 51
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
265
266 51
        return new self($startDate, $startDate->add(new DateInterval('P1D')), $boundaryType);
267
    }
268
269
    /**
270
     * Creates new instance from an ISO 8601 time interval.
271
     * This named constructor only support full datepoint and interval.
272
     *
273
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
274
     *
275
     * @throws \Exception
276
     * @throws Exception
277
     */
278 21
    public static function fromISO8601(
279
        string $isoFormat,
280
        string $separator = '/',
281
        string $boundaryType = self::INCLUDE_START_EXCLUDE_END
282
    ): self {
283
        /** @var string[] $parts */
284 21
        $parts = explode($separator, $isoFormat);
285 21
        if (2 !== count($parts)) {
286 3
            throw new Exception('The submitted format and/or the separator are not valid. Please review your parameters against the ISO8601 interval format.');
287
        }
288
289 18
        [$start, $end] = $parts;
290 18
        if ('P' === $start[0]) {
291 3
            return self::before(
292 3
                str_replace('T', ' ', $end),
293 3
                new DateInterval($start),
294 3
                $boundaryType
295
            );
296
        }
297
298 15
        if ('P' === $end[0]) {
299 3
            return self::after(
300 3
                str_replace('T', ' ', $start),
301 3
                new DateInterval($end),
302 3
                $boundaryType
303
            );
304
        }
305
306 12
        [$startDate, $endDate] = self::normalizeISO8601($parts);
307
308 9
        return new self($startDate, $endDate, $boundaryType);
309
    }
310
311
    /**
312
     * @param string[] $iso8601String
313
     *
314
     * @throws Exception
315
     *
316
     * @return string[]
317
     */
318 12
    private static function normalizeISO8601(array $iso8601String): array
319
    {
320
        $formatter = static function (string $datepoint): string {
321 12
            return str_replace('T', ' ', $datepoint);
322 12
        };
323
324 12
        $iso8601String = array_map($formatter, $iso8601String);
325 12
        [$startDate, $endDate] = $iso8601String;
326 12
        $startLength = strlen($startDate);
327 12
        $endLength = strlen($endDate);
328 12
        $diff = $startLength <=> $endLength;
329 12
        if (-1 === $diff) {
330 3
            throw new Exception('The string format is not valid. Please review your submitted ISO8601 Interval format.');
331
        }
332
333 9
        if (1 === $diff) {
334 6
            return [$startDate, substr($startDate, 0, - $endLength).$endDate];
335
        }
336
337 3
        return $iso8601String;
338
    }
339
340
    /**************************************************
341
     * Basic getters
342
     **************************************************/
343
344
    /**
345
     * Returns the starting datepoint.
346
     */
347 243
    public function getStartDate(): DateTimeImmutable
348
    {
349 243
        return $this->startDate;
350
    }
351
352
    /**
353
     * Returns the ending datepoint.
354
     */
355 219
    public function getEndDate(): DateTimeImmutable
356
    {
357 219
        return $this->endDate;
358
    }
359
360
    /**
361
     * Returns the instance boundary type.
362
     */
363 159
    public function getBoundaryType(): string
364
    {
365 159
        return $this->boundaryType;
366
    }
367
368
    /**
369
     * Returns the instance duration as expressed in seconds.
370
     */
371 36
    public function getTimestampInterval(): float
372
    {
373 36
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
374
    }
375
376
    /**
377
     * Returns the instance duration as a DateInterval object.
378
     */
379 129
    public function getDateInterval(): DateInterval
380
    {
381 129
        return $this->startDate->diff($this->endDate);
382
    }
383
384
    /**************************************************
385
     * String representation
386
     **************************************************/
387
388
    /**
389
     * Returns the string representation as a ISO8601 interval format.
390
     *
391
     * @see Period::toISO8601()
392
     */
393 6
    public function __toString()
394
    {
395 6
        return $this->toISO8601();
396
    }
397
398
    /**
399
     * Returns the string representation as a ISO8601 interval format.
400
     *
401
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
402
     *
403
     */
404 6
    public function toISO8601(): string
405
    {
406 6
        $interval = $this->jsonSerialize();
407
408 6
        return $interval['startDate'].'/'.$interval['endDate'];
409
    }
410
411
    /**
412
     * Returns the JSON representation of an instance.
413
     *
414
     * Based on the JSON representation of dates as
415
     * returned by Javascript Date.toJSON() method.
416
     *
417
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
418
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
419
     *
420
     * @return array<string>
421
     */
422 15
    public function jsonSerialize()
423
    {
424 15
        $utc = new DateTimeZone('UTC');
425
426
        return [
427 15
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
428 15
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
429
        ];
430
    }
431
432
    /**
433
     * Returns the mathematical representation of an instance as a left close, right open interval.
434
     *
435
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
436
     * @see https://php.net/manual/en/function.date.php
437
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
438
     *
439
     * @param string $format the format of the outputted date string
440
     */
441 24
    public function format(string $format): string
442
    {
443 24
        return $this->boundaryType[0]
444 24
            .$this->startDate->format($format)
445 24
            .', '
446 24
            .$this->endDate->format($format)
447 24
            .$this->boundaryType[1];
448
    }
449
450
    /**************************************************
451
     * Boundary related methods
452
     **************************************************/
453
454
    /**
455
     * Tells whether the start datepoint is included in the boundary.
456
     */
457 12
    public function isStartIncluded(): bool
458
    {
459 12
        return '[' === $this->boundaryType[0];
460
    }
461
462
    /**
463
     * Tells whether the start datepoint is excluded from the boundary.
464
     */
465 81
    public function isStartExcluded(): bool
466
    {
467 81
        return '(' === $this->boundaryType[0];
468
    }
469
470
    /**
471
     * Tells whether the end datepoint is included in the boundary.
472
     */
473 12
    public function isEndIncluded(): bool
474
    {
475 12
        return ']' === $this->boundaryType[1];
476
    }
477
478
    /**
479
     * Tells whether the end datepoint is excluded from the boundary.
480
     */
481 81
    public function isEndExcluded(): bool
482
    {
483 81
        return ')' === $this->boundaryType[1];
484
    }
485
486
    /**************************************************
487
     * Duration comparison methods
488
     **************************************************/
489
490
    /**
491
     * Compares two instances according to their duration.
492
     *
493
     * Returns:
494
     * <ul>
495
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
496
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
497
     * <li>  0 if both Interval objects have the same duration</li>
498
     * </ul>
499
     */
500 60
    public function durationCompare(self $interval): int
501
    {
502 60
        return $this->startDate->add($this->getDateInterval())
503 60
            <=> $this->startDate->add($interval->getDateInterval());
504
    }
505
506
    /**
507
     * Tells whether the current instance duration is equal to the submitted one.
508
     */
509 6
    public function durationEquals(self $interval): bool
510
    {
511 6
        return 0 === $this->durationCompare($interval);
512
    }
513
514
    /**
515
     * Tells whether the current instance duration is greater than the submitted one.
516
     */
517 18
    public function durationGreaterThan(self $interval): bool
518
    {
519 18
        return 1 === $this->durationCompare($interval);
520
    }
521
522
    /**
523
     * Tells whether the current instance duration is less than the submitted one.
524
     */
525 12
    public function durationLessThan(self $interval): bool
526
    {
527 12
        return -1 === $this->durationCompare($interval);
528
    }
529
530
    /**************************************************
531
     * Relation methods
532
     **************************************************/
533
534
    /**
535
     * Tells whether an instance is entirely before the specified index.
536
     *
537
     * The index can be a DateTimeInterface object or another Period object.
538
     *
539
     * [--------------------)
540
     *                          [--------------------)
541
     *
542
     * @param mixed $index a datepoint or a Period object
543
     */
544 90
    public function isBefore($index): bool
545
    {
546 90
        if ($index instanceof self) {
547 48
            return $this->endDate < $index->startDate
548 48
                || ($this->endDate == $index->startDate && $this->boundaryType[1] !== $index->boundaryType[0]);
549
        }
550
551 42
        $datepoint = self::getDatepoint($index);
552 42
        return $this->endDate < $datepoint
553 42
            || ($this->endDate == $datepoint && ')' === $this->boundaryType[1]);
554
    }
555
556
    /**
557
     * Tells whether the current instance end date meets the interval start date.
558
     *
559
     * [--------------------)
560
     *                      [--------------------)
561
     */
562 309
    public function bordersOnStart(self $interval): bool
563
    {
564 309
        return $this->endDate == $interval->startDate
565 309
            && '][' !== $this->boundaryType[1].$interval->boundaryType[0];
566
    }
567
568
    /**
569
     * Tells whether two intervals share the same start datepoint
570
     * and the same starting boundary type.
571
     *
572
     *    [----------)
573
     *    [--------------------)
574
     *
575
     * or
576
     *
577
     *    [--------------------)
578
     *    [---------)
579
     *
580
     * @param mixed $index a datepoint or a Period object
581
     */
582 27
    public function isStartedBy($index): bool
583
    {
584 27
        if ($index instanceof self) {
585 15
            return $this->startDate == $index->startDate
586 15
                && $this->boundaryType[0] === $index->boundaryType[0];
587
        }
588
589 12
        $index = self::getDatepoint($index);
590
591 12
        return $index == $this->startDate && '[' === $this->boundaryType[0];
592
    }
593
594
    /**
595
     * Tells whether an instance is fully contained in the specified interval.
596
     *
597
     *     [----------)
598
     * [--------------------)
599
     */
600 39
    public function isDuring(self $interval): bool
601
    {
602 39
        return $interval->containsInterval($this);
603
    }
604
605
    /**
606
     * Tells whether an instance fully contains the specified index.
607
     *
608
     * The index can be a DateTimeInterface object or another Period object.
609
     *
610
     * @param mixed $index a datepoint or a Period object
611
     */
612 144
    public function contains($index): bool
613
    {
614 144
        if ($index instanceof self) {
615 66
            return $this->containsInterval($index);
616
        }
617
618 78
        return $this->containsDatepoint(self::getDatepoint($index), $this->boundaryType);
619
    }
620
621
    /**
622
     * Tells whether an instance fully contains another instance.
623
     *
624
     * [--------------------)
625
     *     [----------)
626
     */
627 66
    private function containsInterval(self $interval): bool
628
    {
629 66
        if ($this->startDate < $interval->startDate && $this->endDate > $interval->endDate) {
630 18
            return true;
631
        }
632
633 63
        if ($this->startDate == $interval->startDate && $this->endDate == $interval->endDate) {
634 21
            return $this->boundaryType === $interval->boundaryType || '[]' === $this->boundaryType;
635
        }
636
637 42
        if ($this->startDate == $interval->startDate) {
638 12
            return ($this->boundaryType[0] === $interval->boundaryType[0] || '[' === $this->boundaryType[0])
639 12
                && $this->containsDatepoint($this->startDate->add($interval->getDateInterval()), $this->boundaryType);
640
        }
641
642 30
        if ($this->endDate == $interval->endDate) {
643 18
            return ($this->boundaryType[1] === $interval->boundaryType[1] || ']' === $this->boundaryType[1])
644 18
                && $this->containsDatepoint($this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
0 ignored issues
show
Bug introduced by
It seems like $this->endDate->sub($interval->getDateInterval()) can also be of type false; however, parameter $datepoint of League\Period\Period::containsDatepoint() does only seem to accept DateTimeInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

644
                && $this->containsDatepoint(/** @scrutinizer ignore-type */ $this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
Loading history...
645
        }
646
647 12
        return false;
648
    }
649
650
    /**
651
     * Tells whether an instance contains a datepoint.
652
     *
653
     * [------|------------)
654
     */
655 108
    private function containsDatepoint(DateTimeInterface $datepoint, string $boundaryType): bool
656
    {
657
        switch ($boundaryType) {
658 108
            case self::EXCLUDE_ALL:
659 9
                return $datepoint > $this->startDate && $datepoint < $this->endDate;
660 99
            case self::INCLUDE_ALL:
661 3
                return $datepoint >= $this->startDate && $datepoint <= $this->endDate;
662 96
            case self::EXCLUDE_START_INCLUDE_END:
663 9
                return $datepoint > $this->startDate && $datepoint <= $this->endDate;
664 87
            case self::INCLUDE_START_EXCLUDE_END:
665
            default:
666 87
                return $datepoint >= $this->startDate && $datepoint < $this->endDate;
667
        }
668
    }
669
670
    /**
671
     * Tells whether two intervals share the same datepoints.
672
     *
673
     * [--------------------)
674
     * [--------------------)
675
     */
676 285
    public function equals(self $interval): bool
677
    {
678 285
        return $this->startDate == $interval->startDate
679 285
            && $this->endDate == $interval->endDate
680 285
            && $this->boundaryType === $interval->boundaryType;
681
    }
682
683
    /**
684
     * Tells whether two intervals share the same end datepoint
685
     * and the same ending boundary type.
686
     *
687
     *              [----------)
688
     *    [--------------------)
689
     *
690
     * or
691
     *
692
     *    [--------------------)
693
     *               [---------)
694
     *
695
     * @param mixed $index a datepoint or a Period object
696
     */
697 24
    public function isEndedBy($index): bool
698
    {
699 24
        if ($index instanceof self) {
700 12
            return $this->endDate == $index->endDate
701 12
                && $this->boundaryType[1] === $index->boundaryType[1];
702
        }
703
704 12
        $index = self::getDatepoint($index);
705
706 12
        return $index == $this->endDate && ']' === $this->boundaryType[1];
707
    }
708
709
    /**
710
     * Tells whether the current instance start date meets the interval end date.
711
     *
712
     *                      [--------------------)
713
     * [--------------------)
714
     */
715 291
    public function bordersOnEnd(self $interval): bool
716
    {
717 291
        return $interval->bordersOnStart($this);
718
    }
719
720
    /**
721
     * Tells whether an interval is entirely after the specified index.
722
     * The index can be a DateTimeInterface object or another Period object.
723
     *
724
     *                          [--------------------)
725
     * [--------------------)
726
     *
727
     * @param mixed $index a datepoint or a Period object
728
     */
729 54
    public function isAfter($index): bool
730
    {
731 54
        if ($index instanceof self) {
732 24
            return $index->isBefore($this);
733
        }
734
735 30
        $datepoint = self::getDatepoint($index);
736 30
        return $this->startDate > $datepoint
737 30
            || ($this->startDate == $datepoint && '(' === $this->boundaryType[0]);
738
    }
739
740
    /**
741
     * Tells whether two intervals abuts.
742
     *
743
     * [--------------------)
744
     *                      [--------------------)
745
     * or
746
     *                      [--------------------)
747
     * [--------------------)
748
     */
749 309
    public function abuts(self $interval): bool
750
    {
751 309
        return $this->bordersOnStart($interval) || $this->bordersOnEnd($interval);
752
    }
753
754
    /**
755
     * Tells whether two intervals overlaps.
756
     *
757
     * [--------------------)
758
     *          [--------------------)
759
     */
760 291
    public function overlaps(self $interval): bool
761
    {
762 291
        return !$this->abuts($interval)
763 291
            && $this->startDate < $interval->endDate
764 291
            && $this->endDate > $interval->startDate;
765
    }
766
767
    /**************************************************
768
     * Manipulating instance duration
769
     **************************************************/
770
771
    /**
772
     * Returns the difference between two instances expressed in seconds.
773
     */
774 6
    public function timestampIntervalDiff(self $interval): float
775
    {
776 6
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
777
    }
778
779
    /**
780
     * Returns the difference between two instances expressed with a DateInterval object.
781
     */
782 12
    public function dateIntervalDiff(self $interval): DateInterval
783
    {
784 12
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
785
    }
786
787
    /**
788
     * Allows iteration over a set of dates and times,
789
     * recurring at regular intervals, over the instance.
790
     *
791
     * @see http://php.net/manual/en/dateperiod.construct.php
792
     *
793
     * @param mixed $duration a Duration
794
     */
795 54
    public function getDatePeriod($duration, int $option = 0): DatePeriod
796
    {
797 54
        return new DatePeriod($this->startDate, self::getDuration($duration), $this->endDate, $option);
798
    }
799
800
    /**
801
     * Allows iteration over a set of dates and times,
802
     * recurring at regular intervals, over the instance backwards starting from
803
     * the instance ending datepoint.
804
     *
805
     * @param mixed $duration a Duration
806
     */
807 24
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
808
    {
809 24
        $duration = self::getDuration($duration);
810 24
        $date = $this->endDate;
811 24
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
812 12
            $date = $this->endDate->sub($duration);
813
        }
814
815 24
        while ($date > $this->startDate) {
816 24
            yield $date;
817 24
            $date = $date->sub($duration);
818
        }
819 24
    }
820
821
    /**
822
     * Allows splitting an instance in smaller Period objects according to a given interval.
823
     *
824
     * The returned iterable Interval set is ordered so that:
825
     * <ul>
826
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
827
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
828
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
829
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
830
     * </ul>
831
     *
832
     * @param mixed $duration a Duration
833
     *
834
     * @return iterable<Period>
835
     */
836 30
    public function split($duration): iterable
837
    {
838 30
        $duration = self::getDuration($duration);
839 30
        foreach ($this->getDatePeriod($duration) as $startDate) {
840 30
            $endDate = $startDate->add($duration);
841 30
            if ($endDate > $this->endDate) {
842 12
                $endDate = $this->endDate;
843
            }
844
845 30
            yield new self($startDate, $endDate, $this->boundaryType);
846
        }
847 30
    }
848
849
    /**
850
     * Allows splitting an instance in smaller Period objects according to a given interval.
851
     *
852
     * The returned iterable Period set is ordered so that:
853
     * <ul>
854
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
855
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
856
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
857
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
858
     * </ul>
859
     *
860
     * @param mixed $duration a Duration
861
     *
862
     * @return iterable<Period>
863
     */
864 18
    public function splitBackwards($duration): iterable
865
    {
866 18
        $endDate = $this->endDate;
867 18
        $duration = self::getDuration($duration);
868
        do {
869 18
            $startDate = $endDate->sub($duration);
870 18
            if ($startDate < $this->startDate) {
871 6
                $startDate = $this->startDate;
872
            }
873 18
            yield new self($startDate, $endDate, $this->boundaryType);
874
875 18
            $endDate = $startDate;
876 18
        } while ($endDate > $this->startDate);
877 18
    }
878
879
    /**************************************************
880
     * Manipulation instance endpoints and boundaries
881
     **************************************************/
882
883
    /**
884
     * Returns the computed intersection between two instances as a new instance.
885
     *
886
     * [--------------------)
887
     *          ∩
888
     *                 [----------)
889
     *          =
890
     *                 [----)
891
     *
892
     * @throws Exception If both objects do not overlaps
893
     */
894 153
    public function intersect(self $interval): self
895
    {
896 153
        if (!$this->overlaps($interval)) {
897 12
            throw new Exception('Both '.self::class.' objects should overlaps');
898
        }
899
900 141
        $startDate = $this->startDate;
901 141
        $endDate = $this->endDate;
902 141
        $boundaryType = $this->boundaryType;
903 141
        if ($interval->startDate > $this->startDate) {
904 132
            $boundaryType[0] = $interval->boundaryType[0];
905 132
            $startDate = $interval->startDate;
906
        }
907
908 141
        if ($interval->endDate < $this->endDate) {
909 33
            $boundaryType[1] = $interval->boundaryType[1];
910 33
            $endDate = $interval->endDate;
911
        }
912
913 141
        $intersect = new self($startDate, $endDate, $boundaryType);
914 141
        if ($intersect->equals($this)) {
915 21
            return $this;
916
        }
917
918 138
        return $intersect;
919
    }
920
921
    /**
922
     * Returns the computed difference between two overlapping instances as
923
     * an array containing Period objects or the null value.
924
     *
925
     * The array will always contains 2 elements:
926
     *
927
     * <ul>
928
     * <li>an NULL filled array if both objects have the same datepoints</li>
929
     * <li>one Period object and NULL if both objects share one datepoint</li>
930
     * <li>two Period objects if both objects share no datepoint</li>
931
     * </ul>
932
     *
933
     * [--------------------)
934
     *          \
935
     *                [-----------)
936
     *          =
937
     * [--------------)  +  [-----)
938
     *
939
     * @return array<null|Period>
940
     */
941 99
    public function diff(self $interval): array
942
    {
943 99
        if ($interval->equals($this)) {
944 12
            return [null, null];
945
        }
946
947 87
        $intersect = $this->intersect($interval);
948 81
        $merge = $this->merge($interval);
949 81
        if ($merge->startDate == $intersect->startDate) {
950 9
            $first = ')' === $intersect->boundaryType[1] ? '[' : '(';
951 9
            $boundary = $first.$merge->boundaryType[1];
952
953 9
            return [$merge->startingOn($intersect->endDate)->withBoundaryType($boundary), null];
954
        }
955
956 75
        if ($merge->endDate == $intersect->endDate) {
957 6
            $last = '(' === $intersect->boundaryType[0] ? ']' : ')';
958 6
            $boundary = $merge->boundaryType[0].$last;
959
960 6
            return [$merge->endingOn($intersect->startDate)->withBoundaryType($boundary), null];
961
        }
962
963 69
        $last = '(' === $intersect->boundaryType[0] ? ']' : ')';
964 69
        $lastBoundary = $merge->boundaryType[0].$last;
965
966 69
        $first = ')' === $intersect->boundaryType[1] ? '[' : '(';
967 69
        $firstBoundary = $first.$merge->boundaryType[1];
968
969
        return [
970 69
            $merge->endingOn($intersect->startDate)->withBoundaryType($lastBoundary),
971 69
            $merge->startingOn($intersect->endDate)->withBoundaryType($firstBoundary),
972
        ];
973
    }
974
975
    /**
976
     * DEPRECATION WARNING! This method will be removed in the next major point release.
977
     *
978
     * @deprecated since version 4.9.0
979
     * @see ::subtract
980
     */
981 3
    public function substract(self $interval): Sequence
982
    {
983 3
        return $this->subtract($interval);
984
    }
985
986
    /**
987
     * Returns the difference set operation between two intervals as a Sequence.
988
     * The Sequence can contain from 0 to 2 Periods depending on the result of
989
     * the operation.
990
     *
991
     * [--------------------)
992
     *          -
993
     *                [-----------)
994
     *          =
995
     * [--------------)
996
     */
997 24
    public function subtract(self $interval): Sequence
998
    {
999 24
        if (!$this->overlaps($interval)) {
1000 9
            return new Sequence($this);
1001
        }
1002
1003
        $filter = function ($item): bool {
1004 18
            return null !== $item && $this->overlaps($item);
1005 18
        };
1006
1007 18
        return new Sequence(...array_filter($this->diff($interval), $filter));
1008
    }
1009
1010
    /**
1011
     * Returns the computed gap between two instances as a new instance.
1012
     *
1013
     * [--------------------)
1014
     *          +
1015
     *                          [----------)
1016
     *          =
1017
     *                      [---)
1018
     *
1019
     * @throws Exception If both instance overlaps
1020
     */
1021 84
    public function gap(self $interval): self
1022
    {
1023 84
        if ($this->overlaps($interval)) {
1024 18
            throw new Exception('Both '.self::class.' objects must not overlaps');
1025
        }
1026
1027 66
        $boundaryType = $this->isEndExcluded() ? '[' : '(';
1028 66
        $boundaryType .= $interval->isStartExcluded() ? ']' : ')';
1029 66
        if ($interval->startDate > $this->startDate) {
1030 66
            return new self($this->endDate, $interval->startDate, $boundaryType);
1031
        }
1032
1033 6
        return new self($interval->endDate, $this->startDate, $this->boundaryType);
1034
    }
1035
1036
    /**
1037
     * Merges one or more instances to return a new instance.
1038
     * The resulting instance represents the largest duration possible.
1039
     *
1040
     * This method MUST retain the state of the current instance, and return
1041
     * an instance that contains the specified new datepoints.
1042
     *
1043
     * [--------------------)
1044
     *          +
1045
     *                 [----------)
1046
     *          =
1047
     * [--------------------------)
1048
     *
1049
     *
1050
     * @param Period ...$intervals
1051
     */
1052 105
    public function merge(self ...$intervals): self
1053
    {
1054 105
        $carry = $this;
1055 105
        foreach ($intervals as $period) {
1056 99
            if ($carry->startDate > $period->startDate) {
1057 33
                $carry = new self(
1058 33
                    $period->startDate,
1059 33
                    $carry->endDate,
1060 33
                    $period->boundaryType[0].$carry->boundaryType[1]
1061
                );
1062
            }
1063
1064 99
            if ($carry->endDate < $period->endDate) {
1065 87
                $carry = new self(
1066 87
                    $carry->startDate,
1067 87
                    $period->endDate,
1068 91
                    $carry->boundaryType[0].$period->boundaryType[1]
1069
                );
1070
            }
1071
        }
1072
1073 105
        return $carry;
1074
    }
1075
1076
1077
    /**************************************************
1078
     * Mutation methods
1079
     **************************************************/
1080
1081
    /**
1082
     * Returns an instance with the specified starting datepoint.
1083
     *
1084
     * This method MUST retain the state of the current instance, and return
1085
     * an instance that contains the specified starting datepoint.
1086
     *
1087
     * @param mixed $startDate the new starting datepoint
1088
     */
1089 120
    public function startingOn($startDate): self
1090
    {
1091 120
        $startDate = self::getDatepoint($startDate);
1092 120
        if ($startDate == $this->startDate) {
1093 6
            return $this;
1094
        }
1095
1096 120
        return new self($startDate, $this->endDate, $this->boundaryType);
1097
    }
1098
1099
    /**
1100
     * Returns an instance with the specified ending datepoint.
1101
     *
1102
     * This method MUST retain the state of the current instance, and return
1103
     * an instance that contains the specified ending datepoint.
1104
     *
1105
     * @param mixed $endDate the new ending datepoint
1106
     */
1107 120
    public function endingOn($endDate): self
1108
    {
1109 120
        $endDate = self::getDatepoint($endDate);
1110 120
        if ($endDate == $this->endDate) {
1111 6
            return $this;
1112
        }
1113
1114 120
        return new self($this->startDate, $endDate, $this->boundaryType);
1115
    }
1116
1117
    /**
1118
     * Returns an instance with the specified boundary type.
1119
     *
1120
     * This method MUST retain the state of the current instance, and return
1121
     * an instance with the specified range type.
1122
     */
1123 87
    public function withBoundaryType(string $boundaryType): self
1124
    {
1125 87
        if ($boundaryType === $this->boundaryType) {
1126 72
            return $this;
1127
        }
1128
1129 45
        return new self($this->startDate, $this->endDate, $boundaryType);
1130
    }
1131
1132
    /**
1133
     * Returns a new instance with a new ending datepoint.
1134
     *
1135
     * This method MUST retain the state of the current instance, and return
1136
     * an instance that contains the specified ending datepoint.
1137
     *
1138
     * @param mixed $duration a Duration
1139
     */
1140 12
    public function withDurationAfterStart($duration): self
1141
    {
1142 12
        return $this->endingOn($this->startDate->add(self::getDuration($duration)));
1143
    }
1144
1145
    /**
1146
     * Returns a new instance with a new starting datepoint.
1147
     *
1148
     * This method MUST retain the state of the current instance, and return
1149
     * an instance that contains the specified starting datepoint.
1150
     *
1151
     * @param mixed $duration a Duration
1152
     */
1153 12
    public function withDurationBeforeEnd($duration): self
1154
    {
1155 12
        return $this->startingOn($this->endDate->sub(self::getDuration($duration)));
1156
    }
1157
1158
    /**
1159
     * Returns a new instance with a new starting datepoint
1160
     * moved forward or backward by the given interval.
1161
     *
1162
     * This method MUST retain the state of the current instance, and return
1163
     * an instance that contains the specified starting datepoint.
1164
     *
1165
     * @param mixed $duration a Duration
1166
     */
1167 18
    public function moveStartDate($duration): self
1168
    {
1169 18
        return $this->startingOn($this->startDate->add(self::getDuration($duration)));
1170
    }
1171
1172
    /**
1173
     * Returns a new instance with a new ending datepoint
1174
     * moved forward or backward by the given interval.
1175
     *
1176
     * This method MUST retain the state of the current instance, and return
1177
     * an instance that contains the specified ending datepoint.
1178
     *
1179
     * @param mixed $duration a Duration
1180
     */
1181 15
    public function moveEndDate($duration): self
1182
    {
1183 15
        return $this->endingOn($this->endDate->add(self::getDuration($duration)));
1184
    }
1185
1186
    /**
1187
     * Returns a new instance where the datepoints
1188
     * are moved forwards or backward simultaneously by the given DateInterval.
1189
     *
1190
     * This method MUST retain the state of the current instance, and return
1191
     * an instance that contains the specified new datepoints.
1192
     *
1193
     * @param mixed $duration a Duration
1194
     */
1195 24
    public function move($duration): self
1196
    {
1197 24
        $duration = self::getDuration($duration);
1198 24
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration), $this->boundaryType);
1199 24
        if ($this->equals($interval)) {
1200 6
            return $this;
1201
        }
1202
1203 24
        return $interval;
1204
    }
1205
1206
    /**
1207
     * Returns an instance where the given DateInterval is simultaneously
1208
     * subtracted from the starting datepoint and added to the ending datepoint.
1209
     *
1210
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
1211
     *
1212
     * This method MUST retain the state of the current instance, and return
1213
     * an instance that contains the specified new datepoints.
1214
     *
1215
     * @param mixed $duration a Duration
1216
     */
1217 24
    public function expand($duration): self
1218
    {
1219 24
        $duration = self::getDuration($duration);
1220 24
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration), $this->boundaryType);
1221 18
        if ($this->equals($interval)) {
1222 6
            return $this;
1223
        }
1224
1225 12
        return $interval;
1226
    }
1227
}
1228