Passed
Pull Request — master (#71)
by ignace nyamagana
02:04
created

Period::fromDatepoint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com).
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use DateInterval;
17
use DatePeriod;
18
use DateTimeImmutable;
19
use DateTimeInterface;
20
use DateTimeZone;
21
use JsonSerializable;
22
use function intdiv;
23
24
/**
25
 * A immutable value object class to manipulate Time interval.
26
 *
27
 * @package League.period
28
 * @author  Ignace Nyamagana Butera <[email protected]>
29
 * @since   1.0.0
30
 */
31
final class Period implements JsonSerializable
32
{
33
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
34
35
    public const CALENDAR_YEAR = 'YEAR';
36
    public const CALENDAR_ISOYEAR = 'ISOYEAR';
37
    public const CALENDAR_SEMESTER = 'SEMESTER';
38
    public const CALENDAR_QUARTER = 'QUARTER';
39
    public const CALENDAR_MONTH = 'MONTH';
40
    public const CALENDAR_ISOWEEK = 'ISOWEEK';
41
    public const CALENDAR_DAY = 'DAY';
42
    public const CALENDAR_HOUR = 'HOUR';
43
    public const CALENDAR_MINUTE = 'MINUTE';
44
    public const CALENDAR_SECOND = 'SECOND';
45
46
    /**
47
     * The starting included datepoint.
48
     *
49
     * @var DateTimeImmutable
50
     */
51 3
    private $startDate;
52
53 3
    /**
54
     * The ending excluded datepoint.
55
     *
56
     * @var DateTimeImmutable
57
     */
58
    private $endDate;
59
60
    /**
61
     * @inheritdoc
62
     */
63
    public static function __set_state(array $interval)
64 255
    {
65
        return new self($interval['startDate'], $interval['endDate']);
66 255
    }
67 255
68 255
    /**
69 39
     * Creates new instance from a starting datepoint and a duration.
70
     */
71 246
    public static function after($datepoint, $duration): self
72 246
    {
73 246
        $datepoint = Datepoint::create($datepoint);
74
75
        return new self($datepoint, $datepoint->add(Duration::create($duration)));
76
    }
77
78
    /**
79
     * Creates new instance from a ending datepoint and a duration.
80
     */
81
    public static function before($datepoint, $duration): self
82 282
    {
83
        $datepoint = Datepoint::create($datepoint);
84 282
85 246
        return new self($datepoint->sub(Duration::create($duration)), $datepoint);
86
    }
87
88 222
    /**
89 78
     * Creates new instance where the given duration is simultaneously
90
     * substracted from and added to the datepoint.
91
     */
92 153
    public static function around($datepoint, $duration): self
93
    {
94
        $datepoint = Datepoint::create($datepoint);
95
        $duration = Duration::create($duration);
96
97
        return new self($datepoint->sub($duration), $datepoint->add($duration));
98
    }
99
100
    /**
101
     * Creates new instance for a specific year.
102
     */
103
    public static function fromYear(int $year): self
104 6
    {
105
        $startDate = (new DateTimeImmutable())->setDate($year, 1, 1)->setTime(0, 0);
106 6
107 3
        return new self($startDate, $startDate->add(new DateInterval('P1Y')));
108
    }
109
110 3
    /**
111
     * Creates new instance for a specific ISO year.
112
     */
113
    public static function fromIsoYear(int $year): self
114
    {
115
        return new self(
116
            (new DateTimeImmutable())->setISODate($year, 1)->setTime(0, 0),
117
            (new DateTimeImmutable())->setISODate(++$year, 1)->setTime(0, 0)
118
        );
119
    }
120
121
    /**
122
     * Creates new instance for a specific year and semester.
123
     */
124
    public static function fromSemester(int $year, int $semester = 1): self
125
    {
126
        $month = (($semester - 1) * 6) + 1;
127
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
128 138
129
        return new self($startDate, $startDate->add(new DateInterval('P6M')));
130 138
    }
131
132 138
    /**
133
     * Creates new instance for a specific year and quarter.
134
     */
135
    public static function fromQuarter(int $year, int $quarter = 1): self
136
    {
137
        $month = (($quarter - 1) * 3) + 1;
138
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
139
140
        return new self($startDate, $startDate->add(new DateInterval('P3M')));
141
    }
142
143
    /**
144
     * Creates new instance for a specific year and month.
145
     */
146
    public static function fromMonth(int $year, int $month = 1): self
147
    {
148
        $startDate = (new DateTimeImmutable())->setDate($year, $month, 1)->setTime(0, 0);
149 177
150
        return new self($startDate, $startDate->add(new DateInterval('P1M')));
151 177
    }
152 27
153
    /**
154
     * Creates new instance for a specific ISO8601 week.
155 165
     */
156 30
    public static function fromIsoWeek(int $year, int $week = 1): self
157
    {
158
        $startDate = (new DateTimeImmutable())->setISODate($year, $week, 1)->setTime(0, 0);
159 153
160
        return new self($startDate, $startDate->add(new DateInterval('P7D')));
161
    }
162
163
    /**
164
     * Creates new instance for a specific year, month and day.
165
     */
166
    public static function fromDay(int $year, int $month = 1, int $day = 1): self
167
    {
168
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime(0, 0);
169
170
        return new self($startDate, $startDate->add(new DateInterval('P1D')));
171
    }
172
173
    /**
174
     * Creates new instance for a specific year, month, day and hour.
175
     */
176
    public static function fromHour(int $year, int $month = 1, int $day = 1, int $hour = 0): self
177 24
    {
178
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime($hour, 0);
179 24
180
        return new self($startDate, $startDate->add(new DateInterval('PT1H')));
181 24
    }
182
183
    /**
184
     * Creates new instance for a specific year, month, day, hour and minute.
185
     */
186
    public static function fromMinute(int $year, int $month = 1, int $day = 1, int $hour = 0, int $minute = 0): self
187
    {
188
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime($hour, $minute);
189
190
        return new self($startDate, $startDate->add(new DateInterval('PT1M')));
191 18
    }
192
193 18
    /**
194 15
     * Creates new instance for a specific year, month, day, hour, minute and second.
195
     */
196 15
    public static function fromSecond(
197
        int $year,
198
        int $month = 1,
199 6
        int $day = 1,
200
        int $hour = 0,
201 6
        int $minute = 0,
202
        int $second = 0
203
    ): self {
204
        $startDate = (new DateTimeImmutable())->setDate($year, $month, $day)->setTime($hour, $minute, $second);
205
206
        return new self($startDate, $startDate->add(new DateInterval('PT1S')));
207
    }
208
209
    /**
210
     * Creates new instance corresponding to a specific datepoint.
211
     */
212
    public static function fromDatepoint($datepoint): self
213 21
    {
214
        $datepoint = Datepoint::create($datepoint);
215 21
216
        return new self($datepoint, $datepoint);
217
    }
218
219
    /**
220
     * Creates a new instance from a datepoint and a calendar reference.
221
     *
222
     * The datepoint is contained or start the interval and the duration is
223
     * equals to the calendar reference duration.
224
     */
225
    public static function fromCalendar($datepoint, string $calendar): self
226 12
    {
227
        $datepoint = Datepoint::create($datepoint);
228 12
        switch ($calendar) {
229 3
            case self::CALENDAR_HOUR:
230 3
                $startDate = $datepoint->setTime((int) $datepoint->format('H'), 0);
231 3
232
                return new self($startDate, $startDate->add(new DateInterval('PT1H')));
233 3
234
            case self::CALENDAR_MINUTE:
235
                $startDate = $datepoint->setTime((int) $datepoint->format('H'), (int) $datepoint->format('i'));
236 12
237 6
                return new self($startDate, $startDate->add(new DateInterval('PT1M')));
238
239 6
            case self::CALENDAR_SECOND:
240
                $startDate = $datepoint->setTime(
241
                    (int) $datepoint->format('H'),
242
                    (int) $datepoint->format('i'),
243
                    (int) $datepoint->format('s')
244
                );
245
246
                return new self($startDate, $startDate->add(new DateInterval('PT1S')));
247
248
            case self::CALENDAR_DAY:
249
                $startDate = $datepoint->setTime(0, 0);
250
251
                return new self($startDate, $startDate->add(new DateInterval('P1D')));
252
253 75
            case self::CALENDAR_ISOWEEK:
254
                $startDate = $datepoint
255 75
                    ->setTime(0, 0)
256 75
                    ->setISODate((int) $datepoint->format('o'), (int) $datepoint->format('W'), 1);
257 51
258
                return new self($startDate, $startDate->add(new DateInterval('P7D')));
259
260 24
            case self::CALENDAR_MONTH:
261
                $startDate = $datepoint
262
                    ->setTime(0, 0)
263
                    ->setDate((int) $datepoint->format('Y'), (int) $datepoint->format('n'), 1);
264
265
                return new self($startDate, $startDate->add(new DateInterval('P1M')));
266
267
            case self::CALENDAR_QUARTER:
268
                $startDate = $datepoint
269
                    ->setTime(0, 0)
270
                    ->setDate((int) $datepoint->format('Y'), (intdiv((int) $datepoint->format('n'), 3) * 3) + 1, 1);
271 12
272
                return new self($startDate, $startDate->add(new DateInterval('P3M')));
273 12
274 3
            case self::CALENDAR_SEMESTER:
275 3
                $startDate = $datepoint
276 3
                    ->setTime(0, 0)
277
                    ->setDate((int) $datepoint->format('Y'), (intdiv((int) $datepoint->format('n'), 6) * 6) + 1, 1);
278 3
279
                return new self($startDate, $startDate->add(new DateInterval('P6M')));
280
281 12
            case self::CALENDAR_YEAR:
282 6
                $startDate = $datepoint->setTime(0, 0)->setDate((int) $datepoint->format('Y'), 1, 1);
283
284 6
                return new self($startDate, $startDate->add(new DateInterval('P1Y')));
285
286
            case self::CALENDAR_ISOYEAR:
287
                $datepoint = $datepoint->setTime(0, 0);
288
                $year = (int) $datepoint->format('o');
289
290
                return new self($datepoint->setISODate($year, 1), $datepoint->setISODate(++$year, 1));
291
292
            default:
293
                throw new Exception('Unknown Calendar interval');
294
        }
295 39
    }
296
297 39
    /**
298 6
     * Creates new instance from a DatePeriod.
299
     */
300 6
    public static function fromDatePeriod(DatePeriod $datePeriod): self
301
    {
302
        return new self($datePeriod->getStartDate(), $datePeriod->getEndDate());
303 36
    }
304
305 30
    /**
306
     * Creates a new instance.
307
     *
308
     * @param mixed $startDate the starting included datepoint
309
     * @param mixed $endDate   the ending excluded datepoint
310
     *
311
     * @throws Exception If $startDate is greater than $endDate
312
     */
313
    public function __construct($startDate, $endDate)
314
    {
315
        $startDate = Datepoint::create($startDate);
316 24
        $endDate = Datepoint::create($endDate);
317
        if ($startDate > $endDate) {
318 24
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
319 3
        }
320 3
        $this->startDate = $startDate;
321 3
        $this->endDate = $endDate;
322 3
    }
323
324
    /**
325 3
     * Returns the starting included datepoint.
326
     */
327
    public function getStartDate(): DateTimeImmutable
328 24
    {
329 24
        return $this->startDate;
330 18
    }
331
332
    /**
333 18
     * Returns the ending excluded datepoint.
334
     */
335
    public function getEndDate(): DateTimeImmutable
336
    {
337
        return $this->endDate;
338
    }
339
340
    /**
341
     * Returns the instance duration as expressed in seconds.
342
     */
343
    public function getTimestampInterval(): float
344
    {
345
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
346 3
    }
347
348 3
    /**
349
     * Returns the instance duration as a DateInterval object.
350 3
     */
351
    public function getDateInterval(): DateInterval
352
    {
353
        return $this->startDate->diff($this->endDate);
354
    }
355
356
    /**
357
     * Allows iteration over a set of dates and times,
358
     * recurring at regular intervals, over the instance.
359
     *
360
     * @see http://php.net/manual/en/dateperiod.construct.php
361
     */
362
    public function getDatePeriod($duration, int $option = 0): DatePeriod
363 3
    {
364
        return new DatePeriod($this->startDate, Duration::create($duration), $this->endDate, $option);
365 3
    }
366
367 3
    /**
368
     * Allows iteration over a set of dates and times,
369
     * recurring at regular intervals, over the instance backwards starting from
370
     * the instance ending datepoint.
371
     */
372
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
373
    {
374
        $duration = Duration::create($duration);
375
        $date = $this->endDate;
376
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
377
            $date = $this->endDate->sub($duration);
378
        }
379
380 3
        while ($date > $this->startDate) {
381
            yield $date;
382 3
            $date = $date->sub($duration);
383
        }
384 3
    }
385
386
    /**
387
     * Returns the string representation as a ISO8601 interval format.
388
     *
389
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
390
     *
391
     * @return string
392
     */
393
    public function __toString()
394
    {
395
        $interval = $this->jsonSerialize();
396
397 3
        return $interval['startDate'].'/'.$interval['endDate'];
398
    }
399 3
400
    /**
401 3
     * Returns the JSON representation of an instance.
402
     *
403
     * Based on the JSON representation of dates as
404
     * returned by Javascript Date.toJSON() method.
405
     *
406
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
407
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
408
     *
409
     * @return array<string>
410
     */
411
    public function jsonSerialize()
412 177
    {
413
        $utc = new DateTimeZone('UTC');
414 177
415
        return [
416
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
417
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
418
        ];
419
    }
420
421
    /**
422
     * Returns the mathematical representation of an instance as a left close, right open interval.
423
     *
424
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
425 162
     * @see https://php.net/manual/en/function.date.php
426
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
427 162
     *
428
     * @param string $format the format of the outputted date string
429
     */
430
    public function format(string $format): string
431
    {
432
        return '['.$this->startDate->format($format).', '.$this->endDate->format($format).')';
433
    }
434
435 18
    /**
436
     * Compares two instances according to their duration.
437 18
     *
438
     * Returns:
439
     * <ul>
440
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
441
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
442
     * <li>  0 if both Interval objects have the same duration</li>
443
     * </ul>
444
     */
445 33
    public function durationCompare(self $interval): int
446
    {
447 33
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
448
    }
449
450
    /**
451
     * Tells whether the current instance duration is equal to the submitted one.
452
     */
453
    public function durationEquals(self $interval): bool
454
    {
455
        return 0 === $this->durationCompare($interval);
456
    }
457
458
    /**
459
     * Tells whether the current instance duration is greater than the submitted one.
460
     */
461
    public function durationGreaterThan(self $interval): bool
462
    {
463
        return 1 === $this->durationCompare($interval);
464
    }
465
466
    /**
467
     * Tells whether the current instance duration is less than the submitted one.
468
     */
469 24
    public function durationLessThan(self $interval): bool
470
    {
471 24
        return -1 === $this->durationCompare($interval);
472
    }
473
474
    /**
475
     * Tells whether two intervals share the same datepoints.
476
     *
477
     * [--------------------)
478
     * [--------------------)
479
     */
480
    public function equals(self $interval): bool
481
    {
482
        return $this->startDate == $interval->startDate
483
            && $this->endDate == $interval->endDate;
484
    }
485
486
    /**
487
     * Tells whether two intervals abuts.
488
     *
489
     * [--------------------)
490
     *                      [--------------------)
491
     * or
492
     *                      [--------------------)
493
     * [--------------------)
494
     */
495
    public function abuts(self $interval): bool
496
    {
497 12
        return $this->startDate == $interval->endDate
498
            || $this->endDate == $interval->startDate;
499 12
    }
500 12
501
    /**
502 12
     * Tells whether two intervals overlaps.
503 12
     *
504 6
     * [--------------------)
505
     *          [--------------------)
506 12
     */
507
    public function overlaps(self $interval): bool
508 9
    {
509 9
        return $this->startDate < $interval->endDate
510 9
            && $this->endDate > $interval->startDate;
511
    }
512
513
    /**
514
     * Tells whether an interval is entirely after the specified index.
515
     * The index can be a DateTimeInterface object or another Period object.
516
     *
517
     *                          [--------------------)
518
     * [--------------------)
519
     */
520
    public function isAfter($index): bool
521
    {
522
        if ($index instanceof self) {
523
            return $this->startDate >= $index->endDate;
524
        }
525
526
        return $this->startDate > Datepoint::create($index);
527
    }
528
529
    /**
530
     * Tells whether an instance is entirely before the specified index.
531
     *
532
     * The index can be a DateTimeInterface object or another Period object.
533
     *
534
     * [--------------------)
535 6
     *                          [--------------------)
536
     */
537 6
    public function isBefore($index): bool
538 6
    {
539
        if ($index instanceof self) {
540 6
            return $this->endDate <= $index->startDate;
541 6
        }
542 3
543
        return $this->endDate <= Datepoint::create($index);
544 6
    }
545
546 6
    /**
547 6
     * Tells whether an instance fully contains the specified index.
548 6
     *
549
     * The index can be a DateTimeInterface object or another Period object.
550
     *
551
     */
552
    public function contains($index): bool
553
    {
554
        if ($index instanceof self) {
555
            return $this->containsInterval($index);
556
        }
557
558 3
        return $this->containsDatepoint(Datepoint::create($index));
559
    }
560 3
561
    /**
562 3
     * Tells whether an instance fully contains another instance.
563
     *
564
     * [--------------------)
565
     *     [----------)
566
     */
567
    private function containsInterval(self $interval): bool
568
    {
569
        return $this->containsDatepoint($interval->startDate)
570
            && ($interval->endDate >= $this->startDate && $interval->endDate <= $this->endDate);
571
    }
572
573 6
    /**
574
     * Tells whether an instance contains a datepoint.
575 6
     *
576 6
     * [------|------------)
577 6
     */
578
    private function containsDatepoint(DateTimeInterface $datepoint): bool
579
    {
580 6
        return $datepoint >= $this->startDate && $datepoint < $this->endDate;
581 6
    }
582
583
    /**
584
     * Allows splitting an instance in smaller Period objects according to a given interval.
585
     *
586
     * The returned iterable Interval set is ordered so that:
587
     * <ul>
588
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
589
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
590
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
591
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
592
     * </ul>
593
     *
594
     * @return iterable<Period>
595
     */
596
    public function split($duration): iterable
597
    {
598
        $duration = Duration::create($duration);
599 21
        foreach ($this->getDatePeriod($duration) as $startDate) {
600
            $endDate = $startDate->add($duration);
601 21
            if ($endDate > $this->endDate) {
602
                $endDate = $this->endDate;
603
            }
604
605
            yield new self($startDate, $endDate);
606
        }
607
    }
608
609
    /**
610
     * Allows splitting an instance in smaller Period objects according to a given interval.
611
     *
612 9
     * The returned iterable Period set is ordered so that:
613
     * <ul>
614 9
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
615
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
616
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
617
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
618
     * </ul>
619
     *
620
     * @return iterable<Period>
621
     */
622
    public function splitBackwards($duration): iterable
623
    {
624
        $endDate = $this->endDate;
625 9
        $duration = Duration::create($duration);
626
        do {
627 9
            $startDate = $endDate->sub($duration);
628
            if ($startDate < $this->startDate) {
629
                $startDate = $this->startDate;
630
            }
631
            yield new self($startDate, $endDate);
632
633
            $endDate = $startDate;
634
        } while ($endDate > $this->startDate);
635
    }
636
637
    /**
638 3
     * Returns the computed intersection between two instances as a new instance.
639
     *
640 3
     * [--------------------)
641
     *          ∩
642
     *                 [----------)
643
     *          =
644
     *                 [----)
645
     *
646
     * @throws Exception If both objects do not overlaps
647
     */
648
    public function intersect(self $interval): self
649
    {
650 18
        if (!$this->overlaps($interval)) {
651
            throw new Exception('Both '.self::class.' objects should overlaps');
652 18
        }
653 18
654
        return new self(
655
            ($interval->startDate > $this->startDate) ? $interval->startDate : $this->startDate,
656
            ($interval->endDate < $this->endDate) ? $interval->endDate : $this->endDate
657
        );
658
    }
659
660
    /**
661
     * Returns the computed difference between two overlapping instances as
662
     * an array containing Period objects or the null value.
663 42
     *
664
     * The array will always contains 2 elements:
665 42
     *
666 42
     * <ul>
667
     * <li>an NULL filled array if both objects have the same datepoints</li>
668
     * <li>one Period object and NULL if both objects share one datepoint</li>
669
     * <li>two Period objects if both objects share no datepoint</li>
670
     * </ul>
671
     *
672
     * [--------------------)
673
     *          \
674
     *                [-----------)
675
     *          =
676 36
     * [--------------)  +  [-----)
677
     *
678 36
     * @return array<null|Period>
679 36
     */
680 36
    public function diff(self $interval): array
681
    {
682
        if ($interval->equals($this)) {
683
            return [null, null];
684
        }
685
686
        $intersect = $this->intersect($interval);
687
        $merge = $this->merge($interval);
688
        if ($merge->startDate == $intersect->startDate) {
689
            return [$merge->startingOn($intersect->endDate), null];
690
        }
691
692
        if ($merge->endDate == $intersect->endDate) {
693
            return [$merge->endingOn($intersect->startDate), null];
694
        }
695
696
        return [
697 9
            $merge->endingOn($intersect->startDate),
698
            $merge->startingOn($intersect->endDate),
699 9
        ];
700 3
    }
701
702
    /**
703 6
     * Returns the computed gap between two instances as a new instance.
704
     *
705
     * [--------------------)
706
     *          +
707
     *                          [----------)
708
     *          =
709
     *                      [---)
710
     *
711
     * @throws Exception If both instance overlaps
712
     */
713
    public function gap(self $interval): self
714
    {
715
        if ($this->overlaps($interval)) {
716
            throw new Exception('Both '.self::class.' objects must not overlaps');
717
        }
718
719
        if ($interval->startDate > $this->startDate) {
720 12
            return new self($this->endDate, $interval->startDate);
721
        }
722 12
723 6
        return new self($interval->endDate, $this->startDate);
724
    }
725
726 6
    /**
727
     * Returns the difference between two instances expressed in seconds.
728
     */
729
    public function timestampIntervalDiff(self $interval): float
730
    {
731
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
732
    }
733
734
    /**
735
     * Returns the difference between two instances expressed with a DateInterval object.
736
     */
737
    public function dateIntervalDiff(self $interval): DateInterval
738
    {
739
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
740
    }
741
742
    /**
743
     * Returns an instance with the specified starting datepoint.
744 24
     *
745
     * This method MUST retain the state of the current instance, and return
746 24
     * an instance that contains the specified starting datepoint.
747 9
     */
748
    public function startingOn($datepoint): self
749
    {
750 24
        $startDate = Datepoint::create($datepoint);
751
        if ($startDate == $this->startDate) {
752
            return $this;
753
        }
754
755
        return new self($startDate, $this->endDate);
756
    }
757
758
    /**
759
     * Returns an instance with the specified ending datepoint.
760
     *
761 9
     * This method MUST retain the state of the current instance, and return
762
     * an instance that contains the specified ending datepoint.
763 9
     */
764 9
    public function endingOn($datepoint): self
765
    {
766
        $endDate = Datepoint::create($datepoint);
767
        if ($endDate == $this->endDate) {
768
            return $this;
769
        }
770
771
        return new self($this->startDate, $endDate);
772
    }
773
774
    /**
775 24
     * Returns a new instance with a new ending datepoint.
776
     *
777 24
     * This method MUST retain the state of the current instance, and return
778
     * an instance that contains the specified ending datepoint.
779 24
     */
780 24
    public function withDurationAfterStart($duration): self
781
    {
782
        return $this->endingOn($this->startDate->add(Duration::create($duration)));
783
    }
784
785
    /**
786
     * Returns a new instance with a new starting datepoint.
787
     *
788
     * This method MUST retain the state of the current instance, and return
789
     * an instance that contains the specified starting datepoint.
790 9
     */
791
    public function withDurationBeforeEnd($duration): self
792 9
    {
793
        return $this->startingOn($this->endDate->sub(Duration::create($duration)));
794
    }
795
796
    /**
797
     * Returns a new instance with a new starting datepoint
798
     * moved forward or backward by the given interval.
799
     *
800
     * This method MUST retain the state of the current instance, and return
801
     * an instance that contains the specified starting datepoint.
802 12
     */
803
    public function moveStartDate($duration): self
804 12
    {
805
        return $this->startingOn($this->startDate->add(Duration::create($duration)));
806
    }
807
808
    /**
809
     * Returns a new instance with a new ending datepoint
810
     * moved forward or backward by the given interval.
811
     *
812
     * This method MUST retain the state of the current instance, and return
813
     * an instance that contains the specified ending datepoint.
814
     */
815
    public function moveEndDate($duration): self
816
    {
817
        return $this->endingOn($this->endDate->add(Duration::create($duration)));
818
    }
819
820
    /**
821 6
     * Returns a new instance where the datepoints
822
     * are moved forwards or backward simultaneously by the given DateInterval.
823 6
     *
824
     * This method MUST retain the state of the current instance, and return
825
     * an instance that contains the specified new datepoints.
826
     */
827
    public function move($duration): self
828
    {
829
        $duration = Duration::create($duration);
830
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration));
831
        if ($this->equals($interval)) {
832
            return $this;
833
        }
834
835
        return $interval;
836
    }
837
838
    /**
839
     * Returns an instance where the given DateInterval is simultaneously
840 6
     * substracted from the starting datepoint and added to the ending datepoint.
841
     *
842 6
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
843
     *
844
     * This method MUST retain the state of the current instance, and return
845
     * an instance that contains the specified new datepoints.
846
     */
847
    public function expand($duration): self
848
    {
849
        $duration = Duration::create($duration);
850
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration));
851
        if ($this->equals($interval)) {
852
            return $this;
853
        }
854
855
        return $interval;
856
    }
857
858
    /**
859
     * Merges one or more instances to return a new instance.
860 9
     * The resulting instance represents the largest duration possible.
861
     *
862 9
     * This method MUST retain the state of the current instance, and return
863
     * an instance that contains the specified new datepoints.
864
     *
865
     * [--------------------)
866
     *          U
867
     *                 [----------)
868
     *          =
869
     * [--------------------------)
870
     *
871
     *
872
     * @param Period ...$intervals
873
     */
874
    public function merge(self $interval, self ...$intervals): self
875
    {
876
        $intervals[] = $interval;
877
        $carry = $this;
878
        foreach ($intervals as $interval) {
879
            if ($carry->startDate > $interval->startDate) {
880 12
                $carry = $carry->startingOn($interval->startDate);
881
            }
882 12
883
            if ($carry->endDate < $interval->endDate) {
884
                $carry = $carry->endingOn($interval->endDate);
885
            }
886
        }
887
888
        return $carry;
889
    }
890
}
891