Completed
Pull Request — master (#80)
by
unknown
14:28
created

Period::fromSemester()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use Sequence;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, League\Period\Sequence. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
Bug introduced by
The type Sequence was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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