Completed
Pull Request — master (#80)
by
unknown
07:35 queued 05:53
created

Period::move()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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

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