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

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

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