Passed
Pull Request — master (#106)
by ignace nyamagana
03:16
created

Period::durationEquals()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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