Completed
Pull Request — master (#73)
by ignace nyamagana
03:20
created

Period::fromYear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

621
                && $this->containsDatepoint(/** @scrutinizer ignore-type */ $this->endDate->sub($interval->getDateInterval()), $this->boundaryType);
Loading history...
622
        }
623
624 9
        return false;
625
    }
626
627
    /**
628
     * Tells whether an instance contains a datepoint.
629
     *
630
     * [------|------------)
631
     */
632 81
    private function containsDatepoint(DateTimeInterface $datepoint, string $boundaryType): bool
633
    {
634
        switch ($boundaryType) {
635 81
            case self::EXCLUDE_ALL:
636 6
                return $datepoint > $this->startDate && $datepoint < $this->endDate;
637 75
            case self::INCLUDE_ALL:
638 3
                return $datepoint >= $this->startDate && $datepoint <= $this->endDate;
639 72
            case self::EXCLUDE_START_INCLUDE_END:
640 6
                return $datepoint > $this->startDate && $datepoint <= $this->endDate;
641 66
            case self::INCLUDE_START_EXCLUDE_END:
642
            default:
643 66
                return $datepoint >= $this->startDate && $datepoint < $this->endDate;
644
        }
645
    }
646
647
    /**************************************************
648
     * operation on duration methods
649
     **************************************************/
650
651
    /**
652
     * Returns the instance duration as expressed in seconds.
653
     */
654 33
    public function getTimestampInterval(): float
655
    {
656 33
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
657
    }
658
659
    /**
660
     * Returns the instance duration as a DateInterval object.
661
     */
662 126
    public function getDateInterval(): DateInterval
663
    {
664 126
        return $this->startDate->diff($this->endDate);
665
    }
666
667
    /**
668
     * Returns the difference between two instances expressed in seconds.
669
     */
670 6
    public function timestampIntervalDiff(self $interval): float
671
    {
672 6
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
673
    }
674
675
    /**
676
     * Returns the difference between two instances expressed with a DateInterval object.
677
     */
678 12
    public function dateIntervalDiff(self $interval): DateInterval
679
    {
680 12
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
681
    }
682
683
    /**
684
     * Allows iteration over a set of dates and times,
685
     * recurring at regular intervals, over the instance.
686
     *
687
     * @see http://php.net/manual/en/dateperiod.construct.php
688
     *
689
     * @param mixed $duration a Duration
690
     */
691 54
    public function getDatePeriod($duration, int $option = 0): DatePeriod
692
    {
693 54
        return new DatePeriod($this->startDate, self::getDuration($duration), $this->endDate, $option);
694
    }
695
696
    /**
697
     * Allows iteration over a set of dates and times,
698
     * recurring at regular intervals, over the instance backwards starting from
699
     * the instance ending datepoint.
700
     *
701
     * @param mixed $duration a Duration
702
     */
703 24
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
704
    {
705 24
        $duration = self::getDuration($duration);
706 24
        $date = $this->endDate;
707 24
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
708 12
            $date = $this->endDate->sub($duration);
709
        }
710
711 24
        while ($date > $this->startDate) {
712 24
            yield $date;
713 24
            $date = $date->sub($duration);
714
        }
715 24
    }
716
717
    /**
718
     * Allows splitting an instance in smaller Period objects according to a given interval.
719
     *
720
     * The returned iterable Interval set is ordered so that:
721
     * <ul>
722
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
723
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
724
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
725
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
726
     * </ul>
727
     *
728
     * @param mixed $duration a Duration
729
     *
730
     * @return iterable<Period>
731
     */
732 30
    public function split($duration): iterable
733
    {
734 30
        $duration = self::getDuration($duration);
735 30
        foreach ($this->getDatePeriod($duration) as $startDate) {
736 30
            $endDate = $startDate->add($duration);
737 30
            if ($endDate > $this->endDate) {
738 12
                $endDate = $this->endDate;
739
            }
740
741 30
            yield new self($startDate, $endDate, $this->boundaryType);
742
        }
743 30
    }
744
745
    /**
746
     * Allows splitting an instance in smaller Period objects according to a given interval.
747
     *
748
     * The returned iterable Period set is ordered so that:
749
     * <ul>
750
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
751
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
752
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
753
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
754
     * </ul>
755
     *
756
     * @param mixed $duration a Duration
757
     *
758
     * @return iterable<Period>
759
     */
760 18
    public function splitBackwards($duration): iterable
761
    {
762 18
        $endDate = $this->endDate;
763 18
        $duration = self::getDuration($duration);
764
        do {
765 18
            $startDate = $endDate->sub($duration);
766 18
            if ($startDate < $this->startDate) {
767 6
                $startDate = $this->startDate;
768
            }
769 18
            yield new self($startDate, $endDate, $this->boundaryType);
770
771 18
            $endDate = $startDate;
772 18
        } while ($endDate > $this->startDate);
773 18
    }
774
775
    /**************************************************
776
     * operation on relation methods
777
     **************************************************/
778
779
    /**
780
     * Returns the computed intersection between two instances as a new instance.
781
     *
782
     * [--------------------)
783
     *          ∩
784
     *                 [----------)
785
     *          =
786
     *                 [----)
787
     *
788
     * @throws Exception If both objects do not overlaps
789
     */
790 42
    public function intersect(self $interval): self
791
    {
792 42
        if (!$this->overlaps($interval)) {
793 12
            throw new Exception('Both '.self::class.' objects should overlaps');
794
        }
795
796 30
        $startDate = $this->startDate;
797 30
        $endDate = $this->endDate;
798 30
        $boundaryType = $this->boundaryType;
799 30
        if ($interval->startDate > $this->startDate) {
800 24
            $startDate = $interval->startDate;
801 24
            $boundaryType[0] = $interval->boundaryType[0];
802
        }
803
804 30
        if ($interval->endDate < $this->endDate) {
805 24
            $endDate = $interval->endDate;
806 24
            $boundaryType[1] = $interval->boundaryType[1];
807
        }
808
809 30
        $intersect = new self($startDate, $endDate, $boundaryType);
810 30
        if ($intersect->equals($this)) {
811 12
            return $this;
812
        }
813
814 30
        return $intersect;
815
    }
816
817
    /**
818
     * Returns the computed difference between two overlapping instances as
819
     * an array containing Period objects or the null value.
820
     *
821
     * The array will always contains 2 elements:
822
     *
823
     * <ul>
824
     * <li>an NULL filled array if both objects have the same datepoints</li>
825
     * <li>one Period object and NULL if both objects share one datepoint</li>
826
     * <li>two Period objects if both objects share no datepoint</li>
827
     * </ul>
828
     *
829
     * [--------------------)
830
     *          \
831
     *                [-----------)
832
     *          =
833
     * [--------------)  +  [-----)
834
     *
835
     * @return array<null|Period>
836
     */
837 30
    public function diff(self $interval): array
838
    {
839 30
        if ($interval->equals($this)) {
840 6
            return [null, null];
841
        }
842
843 24
        $intersect = $this->intersect($interval);
844 18
        $merge = $this->merge($interval);
845 18
        if ($merge->startDate == $intersect->startDate) {
846 6
            return [$merge->startingOn($intersect->endDate), null];
847
        }
848
849 12
        if ($merge->endDate == $intersect->endDate) {
850 6
            return [$merge->endingOn($intersect->startDate), null];
851
        }
852
853
        return [
854 6
            $merge->endingOn($intersect->startDate),
855 6
            $merge->startingOn($intersect->endDate),
856
        ];
857
    }
858
859
    /**
860
     * Returns the computed gap between two instances as a new instance.
861
     *
862
     * [--------------------)
863
     *          +
864
     *                          [----------)
865
     *          =
866
     *                      [---)
867
     *
868
     * @throws Exception If both instance overlaps
869
     */
870 36
    public function gap(self $interval): self
871
    {
872 36
        if ($this->overlaps($interval)) {
873 18
            throw new Exception('Both '.self::class.' objects must not overlaps');
874
        }
875
876 18
        $boundaryType = $this->isEndDateExcluded() ? '[' : '(';
877 18
        $boundaryType .= $interval->isStartDateExcluded() ? ']' : ')';
878 18
        if ($interval->startDate > $this->startDate) {
879 18
            return new self($this->endDate, $interval->startDate, $boundaryType);
880
        }
881
882 6
        return new self($interval->endDate, $this->startDate, $this->boundaryType);
883
    }
884
885
    /**
886
     * Merges one or more instances to return a new instance.
887
     * The resulting instance represents the largest duration possible.
888
     *
889
     * This method MUST retain the state of the current instance, and return
890
     * an instance that contains the specified new datepoints.
891
     *
892
     * [--------------------)
893
     *          +
894
     *                 [----------)
895
     *          =
896
     * [--------------------------)
897
     *
898
     *
899
     * @param Period ...$intervals
900
     */
901 33
    public function merge(self $interval, self ...$intervals): self
902
    {
903 33
        $intervals[] = $interval;
904 33
        $carry = $this;
905 33
        foreach ($intervals as $interval) {
906 33
            if ($carry->startDate > $interval->startDate) {
907 21
                $carry = new self(
908 21
                    $interval->startDate,
909 21
                    $carry->endDate,
910 21
                    $interval->boundaryType[0].$carry->boundaryType[1]
911
                );
912
            }
913
914 33
            if ($carry->endDate < $interval->endDate) {
915 21
                $carry = new self(
916 21
                    $carry->startDate,
917 21
                    $interval->endDate,
918 33
                    $carry->boundaryType[0].$interval->boundaryType[1]
919
                );
920
            }
921
        }
922
923 33
        return $carry;
924
    }
925
926
927
    /**************************************************
928
     * mutation methods
929
     **************************************************/
930
931
    /**
932
     * Returns an instance with the specified starting datepoint.
933
     *
934
     * This method MUST retain the state of the current instance, and return
935
     * an instance that contains the specified starting datepoint.
936
     *
937
     * @param mixed $startDate the new starting datepoint
938
     */
939 57
    public function startingOn($startDate): self
940
    {
941 57
        $startDate = self::getDatepoint($startDate);
942 57
        if ($startDate == $this->startDate) {
943 6
            return $this;
944
        }
945
946 57
        return new self($startDate, $this->endDate, $this->boundaryType);
947
    }
948
949
    /**
950
     * Returns an instance with the specified ending datepoint.
951
     *
952
     * This method MUST retain the state of the current instance, and return
953
     * an instance that contains the specified ending datepoint.
954
     *
955
     * @param mixed $endDate the new ending datepoint
956
     */
957 57
    public function endingOn($endDate): self
958
    {
959 57
        $endDate = self::getDatepoint($endDate);
960 57
        if ($endDate == $this->endDate) {
961 6
            return $this;
962
        }
963
964 57
        return new self($this->startDate, $endDate, $this->boundaryType);
965
    }
966
967
    /**
968
     * Returns an instance with the specified boundary type.
969
     *
970
     * This method MUST retain the state of the current instance, and return
971
     * an instance with the specified range type.
972
     */
973 6
    public function withBoundaryType(string $boundaryType): self
974
    {
975 6
        if ($boundaryType === $this->boundaryType) {
976 3
            return $this;
977
        }
978
979 6
        return new self($this->startDate, $this->endDate, $boundaryType);
980
    }
981
982
    /**
983
     * Returns a new instance with a new ending datepoint.
984
     *
985
     * This method MUST retain the state of the current instance, and return
986
     * an instance that contains the specified ending datepoint.
987
     *
988
     * @param mixed $duration a Duration
989
     */
990 12
    public function withDurationAfterStart($duration): self
991
    {
992 12
        return $this->endingOn($this->startDate->add(self::getDuration($duration)));
993
    }
994
995
    /**
996
     * Returns a new instance with a new starting datepoint.
997
     *
998
     * This method MUST retain the state of the current instance, and return
999
     * an instance that contains the specified starting datepoint.
1000
     *
1001
     * @param mixed $duration a Duration
1002
     */
1003 12
    public function withDurationBeforeEnd($duration): self
1004
    {
1005 12
        return $this->startingOn($this->endDate->sub(self::getDuration($duration)));
1006
    }
1007
1008
    /**
1009
     * Returns a new instance with a new starting datepoint
1010
     * moved forward or backward by the given interval.
1011
     *
1012
     * This method MUST retain the state of the current instance, and return
1013
     * an instance that contains the specified starting datepoint.
1014
     *
1015
     * @param mixed $duration a Duration
1016
     */
1017 18
    public function moveStartDate($duration): self
1018
    {
1019 18
        return $this->startingOn($this->startDate->add(self::getDuration($duration)));
1020
    }
1021
1022
    /**
1023
     * Returns a new instance with a new ending datepoint
1024
     * moved forward or backward by the given interval.
1025
     *
1026
     * This method MUST retain the state of the current instance, and return
1027
     * an instance that contains the specified ending datepoint.
1028
     *
1029
     * @param mixed $duration a Duration
1030
     */
1031 15
    public function moveEndDate($duration): self
1032
    {
1033 15
        return $this->endingOn($this->endDate->add(self::getDuration($duration)));
1034
    }
1035
1036
    /**
1037
     * Returns a new instance where the datepoints
1038
     * are moved forwards or backward simultaneously by the given DateInterval.
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
     * @param mixed $duration a Duration
1044
     */
1045 24
    public function move($duration): self
1046
    {
1047 24
        $duration = self::getDuration($duration);
1048 24
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration), $this->boundaryType);
1049 24
        if ($this->equals($interval)) {
1050 6
            return $this;
1051
        }
1052
1053 24
        return $interval;
1054
    }
1055
1056
    /**
1057
     * Returns an instance where the given DateInterval is simultaneously
1058
     * substracted from the starting datepoint and added to the ending datepoint.
1059
     *
1060
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
1061
     *
1062
     * This method MUST retain the state of the current instance, and return
1063
     * an instance that contains the specified new datepoints.
1064
     *
1065
     * @param mixed $duration a Duration
1066
     */
1067 24
    public function expand($duration): self
1068
    {
1069 24
        $duration = self::getDuration($duration);
1070 24
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration), $this->boundaryType);
1071 18
        if ($this->equals($interval)) {
1072 6
            return $this;
1073
        }
1074
1075 12
        return $interval;
1076
    }
1077
}
1078