Completed
Push — master ( a63d58...266d26 )
by ignace nyamagana
14:09
created

Period::containsInterval()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
ccs 3
cts 3
cp 1
crap 3
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com).
5
 *
6
 * @author  Ignace Nyamagana Butera <[email protected]>
7
 * @license https://github.com/thephpleague/period/blob/master/LICENSE (MIT License)
8
 * @version 4.0.0
9
 * @link    https://github.com/thephpleague/period
10
 *
11
 * For the full copyright and license information, please view the LICENSE
12
 * file that was distributed with this source code.
13
 */
14
15
declare(strict_types=1);
16
17
namespace League\Period;
18
19
use DateInterval;
20
use DatePeriod;
21
use DateTimeImmutable;
22
use DateTimeInterface;
23
use DateTimeZone;
24
use JsonSerializable;
25
26
/**
27
 * A immutable value object class to manipulate Time interval.
28
 *
29
 * @package League.period
30
 * @author  Ignace Nyamagana Butera <[email protected]>
31
 * @since   1.0.0
32
 */
33
final class Period implements JsonSerializable
34
{
35
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
36
37
    /**
38
     * The starting included datepoint.
39
     *
40
     * @var DateTimeImmutable
41
     */
42
    private $startDate;
43
44
    /**
45
     * The ending excluded datepoint.
46
     *
47
     * @var DateTimeImmutable
48
     */
49
    private $endDate;
50
51 3
    /**
52
     * @inheritdoc
53 3
     */
54
    public static function __set_state(array $interval)
55
    {
56
        return new self($interval['startDate'], $interval['endDate']);
57
    }
58
59
    /**
60
     * Creates a new instance.
61
     *
62
     * @param mixed $startDate the starting included datepoint
63
     * @param mixed $endDate   the ending excluded datepoint
64 255
     *
65
     * @throws Exception If $startDate is greater than $endDate
66 255
     */
67 255
    public function __construct($startDate, $endDate)
68 255
    {
69 39
        $startDate = datepoint($startDate);
70
        $endDate = datepoint($endDate);
71 246
        if ($startDate > $endDate) {
72 246
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
73 246
        }
74
        $this->startDate = $startDate;
75
        $this->endDate = $endDate;
76
    }
77
78
    /**
79
     * Returns the starting included datepoint.
80
     */
81
    public function getStartDate(): DateTimeImmutable
82 282
    {
83
        return $this->startDate;
84 282
    }
85 246
86
    /**
87
     * Returns the ending excluded datepoint.
88 222
     */
89 78
    public function getEndDate(): DateTimeImmutable
90
    {
91
        return $this->endDate;
92 153
    }
93
94
    /**
95
     * Returns the instance duration as expressed in seconds.
96
     */
97
    public function getTimestampInterval(): float
98
    {
99
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
100
    }
101
102
    /**
103
     * Returns the instance duration as a DateInterval object.
104 6
     */
105
    public function getDateInterval(): DateInterval
106 6
    {
107 3
        return $this->startDate->diff($this->endDate);
108
    }
109
110 3
    /**
111
     * Allows iteration over a set of dates and times,
112
     * recurring at regular intervals, over the instance.
113
     *
114
     * @see http://php.net/manual/en/dateperiod.construct.php
115
     */
116
    public function getDatePeriod($duration, int $option = 0): DatePeriod
117
    {
118
        return new DatePeriod($this->startDate, duration($duration), $this->endDate, $option);
119
    }
120
121
    /**
122
     * Allows iteration over a set of dates and times,
123
     * recurring at regular intervals, over the instance backwards starting from
124
     * the instance ending datepoint.
125
     */
126
    public function getDatePeriodBackwards($duration, int $option = 0): iterable
127
    {
128 138
        $duration = duration($duration);
129
        $date = $this->endDate;
130 138
        if ((bool) ($option & DatePeriod::EXCLUDE_START_DATE)) {
131
            $date = $this->endDate->sub($duration);
132 138
        }
133
134
        while ($date > $this->startDate) {
135
            yield $date;
136
            $date = $date->sub($duration);
137
        }
138
    }
139
140
    /**
141
     * Returns the string representation as a ISO8601 interval format.
142
     *
143
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
144
     *
145
     * @return string
146
     */
147
    public function __toString()
148
    {
149 177
        $interval = $this->jsonSerialize();
150
151 177
        return $interval['startDate'].'/'.$interval['endDate'];
152 27
    }
153
154
    /**
155 165
     * Returns the JSON representation of an instance.
156 30
     *
157
     * Based on the JSON representation of dates as
158
     * returned by Javascript Date.toJSON() method.
159 153
     *
160
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
161
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
162
     *
163
     * @return array<string>
164
     */
165
    public function jsonSerialize()
166
    {
167
        $utc = new DateTimeZone('UTC');
168
169
        return [
170
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
171
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
172
        ];
173
    }
174
175
    /**
176
     * Returns the mathematical representation of an instance as a left close, right open interval.
177 24
     *
178
     * @see https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals
179 24
     * @see https://php.net/manual/en/function.date.php
180
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
181 24
     *
182
     * @param string $format the format of the outputted date string
183
     */
184
    public function format(string $format): string
185
    {
186
        return '['.$this->startDate->format($format).', '.$this->endDate->format($format).')';
187
    }
188
189
    /**
190
     * Compares two instances according to their duration.
191 18
     *
192
     * Returns:
193 18
     * <ul>
194 15
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
195
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
196 15
     * <li>  0 if both Interval objects have the same duration</li>
197
     * </ul>
198
     */
199 6
    public function durationCompare(self $interval): int
200
    {
201 6
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
202
    }
203
204
    /**
205
     * Tells whether the current instance duration is equal to the submitted one.
206
     */
207
    public function durationEquals(self $interval): bool
208
    {
209
        return 0 === $this->durationCompare($interval);
210
    }
211
212
    /**
213 21
     * Tells whether the current instance duration is greater than the submitted one.
214
     */
215 21
    public function durationGreaterThan(self $interval): bool
216
    {
217
        return 1 === $this->durationCompare($interval);
218
    }
219
220
    /**
221
     * Tells whether the current instance duration is less than the submitted one.
222
     */
223
    public function durationLessThan(self $interval): bool
224
    {
225
        return -1 === $this->durationCompare($interval);
226 12
    }
227
228 12
    /**
229 3
     * Tells whether two intervals share the same datepoints.
230 3
     *
231 3
     * [--------------------)
232
     * [--------------------)
233 3
     */
234
    public function equals(self $interval): bool
235
    {
236 12
        return $this->startDate == $interval->startDate
237 6
            && $this->endDate == $interval->endDate;
238
    }
239 6
240
    /**
241
     * Tells whether two intervals abuts.
242
     *
243
     * [--------------------)
244
     *                      [--------------------)
245
     * or
246
     *                      [--------------------)
247
     * [--------------------)
248
     */
249
    public function abuts(self $interval): bool
250
    {
251
        return $this->startDate == $interval->endDate
252
            || $this->endDate == $interval->startDate;
253 75
    }
254
255 75
    /**
256 75
     * Tells whether two intervals overlaps.
257 51
     *
258
     * [--------------------)
259
     *          [--------------------)
260 24
     */
261
    public function overlaps(self $interval): bool
262
    {
263
        return $this->startDate < $interval->endDate
264
            && $this->endDate > $interval->startDate;
265
    }
266
267
    /**
268
     * Tells whether an interval is entirely after the specified index.
269
     * The index can be a DateTimeInterface object or another Period object.
270
     *
271 12
     *                          [--------------------)
272
     * [--------------------)
273 12
     */
274 3
    public function isAfter($index): bool
275 3
    {
276 3
        if ($index instanceof self) {
277
            return $this->startDate >= $index->endDate;
278 3
        }
279
280
        return $this->startDate > datepoint($index);
281 12
    }
282 6
283
    /**
284 6
     * Tells whether an instance is entirely before the specified index.
285
     *
286
     * The index can be a DateTimeInterface object or another Period object.
287
     *
288
     * [--------------------)
289
     *                          [--------------------)
290
     */
291
    public function isBefore($index): bool
292
    {
293
        if ($index instanceof self) {
294
            return $this->endDate <= $index->startDate;
295 39
        }
296
297 39
        return $this->endDate <= datepoint($index);
298 6
    }
299
300 6
    /**
301
     * Tells whether an instance fully contains the specified index.
302
     *
303 36
     * The index can be a DateTimeInterface object or another Period object.
304
     *
305 30
     */
306
    public function contains($index): bool
307
    {
308
        if ($index instanceof self) {
309
            return $this->containsInterval($index);
310
        }
311
312
        return $this->containsDatePoint(datepoint($index));
313
    }
314
315
    /**
316 24
     * Tells whether an instance fully contains another instance.
317
     *
318 24
     * [--------------------)
319 3
     *     [----------)
320 3
     */
321 3
    private function containsInterval(self $interval): bool
322 3
    {
323
        return $this->containsDatePoint($interval->startDate)
324
            && ($interval->endDate >= $this->startDate && $interval->endDate <= $this->endDate);
325 3
    }
326
327
    /**
328 24
     * Tells whether an instance contains a datepoint.
329 24
     *
330 18
     * [------|------------)
331
     */
332
    private function containsDatePoint(DateTimeInterface $datepoint): bool
333 18
    {
334
        return $datepoint >= $this->startDate && $datepoint < $this->endDate;
335
    }
336
337
    /**
338
     * Allows splitting an instance in smaller Period objects according to a given interval.
339
     *
340
     * The returned iterable Interval set is ordered so that:
341
     * <ul>
342
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
343
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
344
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
345
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
346 3
     * </ul>
347
     *
348 3
     * @return iterable<Period>
0 ignored issues
show
Documentation introduced by
The doc-type iterable<Period> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
349
     */
350 3
    public function split($duration): iterable
351
    {
352
        $duration = duration($duration);
353
        foreach ($this->getDatePeriod($duration) as $startDate) {
354
            $endDate = $startDate->add($duration);
355
            if ($endDate > $this->endDate) {
356
                $endDate = $this->endDate;
357
            }
358
359
            yield new self($startDate, $endDate);
360
        }
361
362
363 3
        /*
364
        $startDate = $this->startDate;
365 3
        $duration = duration($duration);
366
        do {
367 3
            $endDate = $startDate->add($duration);
368
            if ($endDate > $this->endDate) {
369
                $endDate = $this->endDate;
370
            }
371
            yield new self($startDate, $endDate);
372
373
            $startDate = $endDate;
374
        } while ($startDate < $this->endDate);
375
        */
376
    }
377
378
    /**
379
     * Allows splitting an instance in smaller Period objects according to a given interval.
380 3
     *
381
     * The returned iterable Period set is ordered so that:
382 3
     * <ul>
383
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
384 3
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
385
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
386
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
387
     * </ul>
388
     *
389
     * @return iterable<Period>
0 ignored issues
show
Documentation introduced by
The doc-type iterable<Period> could not be parsed: Expected "|" or "end of type", but got "<" at position 8. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
390
     */
391
    public function splitBackwards($duration): iterable
392
    {
393
        $endDate = $this->endDate;
394
        $duration = duration($duration);
395
        do {
396
            $startDate = $endDate->sub($duration);
397 3
            if ($startDate < $this->startDate) {
398
                $startDate = $this->startDate;
399 3
            }
400
            yield new self($startDate, $endDate);
401 3
402
            $endDate = $startDate;
403
        } while ($endDate > $this->startDate);
404
    }
405
406
    /**
407
     * Returns the computed intersection between two instances as a new instance.
408
     *
409
     * [--------------------)
410
     *          ∩
411
     *                 [----------)
412 177
     *          =
413
     *                 [----)
414 177
     *
415
     * @throws Exception If both objects do not overlaps
416
     */
417
    public function intersect(self $interval): self
418
    {
419
        if (!$this->overlaps($interval)) {
420
            throw new Exception('Both '.self::class.' objects should overlaps');
421
        }
422
423
        return new self(
424
            ($interval->startDate > $this->startDate) ? $interval->startDate : $this->startDate,
425 162
            ($interval->endDate < $this->endDate) ? $interval->endDate : $this->endDate
426
        );
427 162
    }
428
429
    /**
430
     * Returns the computed difference between two overlapping instances as
431
     * an array containing Period objects or the null value.
432
     *
433
     * The array will always contains 2 elements:
434
     *
435 18
     * <ul>
436
     * <li>an NULL filled array if both objects have the same datepoints</li>
437 18
     * <li>one Period object and NULL if both objects share one datepoint</li>
438
     * <li>two Period objects if both objects share no datepoint</li>
439
     * </ul>
440
     *
441
     * [--------------------)
442
     *          -
443
     *                [-----------)
444
     *          =
445 33
     * [--------------)  +  [-----)
446
     *
447 33
     * @return array<null|Period>
448
     */
449
    public function diff(self $interval): array
450
    {
451
        if ($interval->equals($this)) {
452
            return [null, null];
453
        }
454
455
        $intersect = $this->intersect($interval);
456
        $merge = $this->merge($interval);
457
        if ($merge->startDate == $intersect->startDate) {
458
            return [$merge->startingOn($intersect->endDate), null];
459
        }
460
461
        if ($merge->endDate == $intersect->endDate) {
462
            return [$merge->endingOn($intersect->startDate), null];
463
        }
464
465
        return [
466
            $merge->endingOn($intersect->startDate),
467
            $merge->startingOn($intersect->endDate),
468
        ];
469 24
    }
470
471 24
    /**
472
     * Returns the computed gap between two instances as a new instance.
473
     *
474
     * [--------------------)
475
     *          +
476
     *                          [----------)
477
     *          =
478
     *                      [---)
479
     *
480
     * @throws Exception If both instance overlaps
481
     */
482
    public function gap(self $interval): self
483
    {
484
        if ($this->overlaps($interval)) {
485
            throw new Exception('Both '.self::class.' objects must not overlaps');
486
        }
487
488
        if ($interval->startDate > $this->startDate) {
489
            return new self($this->endDate, $interval->startDate);
490
        }
491
492
        return new self($interval->endDate, $this->startDate);
493
    }
494
495
    /**
496
     * Returns the difference between two instances expressed in seconds.
497 12
     */
498
    public function timestampIntervalDiff(self $interval): float
499 12
    {
500 12
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
501
    }
502 12
503 12
    /**
504 6
     * Returns the difference between two instances expressed with a DateInterval object.
505
     */
506 12
    public function dateIntervalDiff(self $interval): DateInterval
507
    {
508 9
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
509 9
    }
510 9
511
    /**
512
     * Returns an instance with the specified starting datepoint.
513
     *
514
     * This method MUST retain the state of the current instance, and return
515
     * an instance that contains the specified starting datepoint.
516
     */
517
    public function startingOn($datepoint): self
518
    {
519
        $startDate = datepoint($datepoint);
520
        if ($startDate == $this->startDate) {
521
            return $this;
522
        }
523
524
        return new self($startDate, $this->endDate);
525
    }
526
527
    /**
528
     * Returns an instance with the specified ending datepoint.
529
     *
530
     * This method MUST retain the state of the current instance, and return
531
     * an instance that contains the specified ending datepoint.
532
     */
533
    public function endingOn($datepoint): self
534
    {
535 6
        $endDate = datepoint($datepoint);
536
        if ($endDate == $this->endDate) {
537 6
            return $this;
538 6
        }
539
540 6
        return new self($this->startDate, $endDate);
541 6
    }
542 3
543
    /**
544 6
     * Returns a new instance with a new ending datepoint.
545
     *
546 6
     * This method MUST retain the state of the current instance, and return
547 6
     * an instance that contains the specified ending datepoint.
548 6
     */
549
    public function withDurationAfterStart($duration): self
550
    {
551
        return $this->endingOn($this->startDate->add(duration($duration)));
552
    }
553
554
    /**
555
     * Returns a new instance with a new starting datepoint.
556
     *
557
     * This method MUST retain the state of the current instance, and return
558 3
     * an instance that contains the specified starting datepoint.
559
     */
560 3
    public function withDurationBeforeEnd($duration): self
561
    {
562 3
        return $this->startingOn($this->endDate->sub(duration($duration)));
563
    }
564
565
    /**
566
     * Returns a new instance with a new starting datepoint
567
     * moved forward or backward by the given interval.
568
     *
569
     * This method MUST retain the state of the current instance, and return
570
     * an instance that contains the specified starting datepoint.
571
     */
572
    public function moveStartDate($duration): self
573 6
    {
574
        return $this->startingOn($this->startDate->add(duration($duration)));
575 6
    }
576 6
577 6
    /**
578
     * Returns a new instance with a new ending datepoint
579
     * moved forward or backward by the given interval.
580 6
     *
581 6
     * This method MUST retain the state of the current instance, and return
582
     * an instance that contains the specified ending datepoint.
583
     */
584
    public function moveEndDate($duration): self
585
    {
586
        return $this->endingOn($this->endDate->add(duration($duration)));
587
    }
588
589
    /**
590
     * Returns a new instance where the datepoints
591
     * are moved forwards or backward simultaneously by the given DateInterval.
592
     *
593
     * This method MUST retain the state of the current instance, and return
594
     * an instance that contains the specified new datepoints.
595
     */
596
    public function move($duration): self
597
    {
598
        $duration = duration($duration);
599 21
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration));
600
        if ($this->equals($interval)) {
601 21
            return $this;
602
        }
603
604
        return $interval;
605
    }
606
607
    /**
608
     * Returns an instance where the given DateInterval is simultaneously
609
     * substracted from the starting datepoint and added to the ending datepoint.
610
     *
611
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
612 9
     *
613
     * This method MUST retain the state of the current instance, and return
614 9
     * an instance that contains the specified new datepoints.
615
     */
616
    public function expand($duration): self
617
    {
618
        $duration = duration($duration);
619
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration));
620
        if ($this->equals($interval)) {
621
            return $this;
622
        }
623
624
        return $interval;
625 9
    }
626
627 9
    /**
628
     * Merges one or more instances to return a new instance.
629
     * The resulting instance represents the largest duration possible.
630
     *
631
     * This method MUST retain the state of the current instance, and return
632
     * an instance that contains the specified new datepoints.
633
     *
634
     * @param Period ...$intervals
635
     */
636
    public function merge(self $interval, self ...$intervals): self
637
    {
638 3
        $intervals[] = $interval;
639
        $carry = $this;
640 3
        foreach ($intervals as $interval) {
641
            if ($carry->startDate > $interval->startDate) {
642
                $carry = $carry->startingOn($interval->startDate);
643
            }
644
645
            if ($carry->endDate < $interval->endDate) {
646
                $carry = $carry->endingOn($interval->endDate);
647
            }
648
        }
649
650 18
        return $carry;
651
    }
652
}
653