Completed
Push — master ( bd9527...dff488 )
by ignace nyamagana
05:23
created

Period::isDuring()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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