Completed
Push — master ( b0ed12...5021a1 )
by ignace nyamagana
11:41
created

Period::createFromDatePeriod()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
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
use function array_unshift;
26
use function sprintf;
27
28
/**
29
 * A immutable value object class to manipulate Time interval.
30
 *
31
 * @package League.period
32
 * @author  Ignace Nyamagana Butera <[email protected]>
33
 * @since   1.0.0
34
 */
35
final class Period implements JsonSerializable
36
{
37
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
38
39
    /**
40
     * Period starting included datepoint.
41
     *
42
     * @var DateTimeImmutable
43
     */
44
    private $startDate;
45
46
    /**
47
     * Period ending excluded datepoint.
48
     *
49
     * @var DateTimeImmutable
50
     */
51 3
    private $endDate;
52
53 3
    /**
54
     * @inheritdoc
55
     */
56
    public static function __set_state(array $interval)
57
    {
58
        return new self($interval['startDate'], $interval['endDate']);
59
    }
60
61
    /**
62
     * Creates a new instance.
63
     *
64 255
     * @param mixed $startDate the interval start datepoint
65
     * @param mixed $endDate   the interval end datepoint
66 255
     *
67 255
     * @throws Exception If $startDate is greater than $endDate
68 255
     */
69 39
    public function __construct($startDate, $endDate)
70
    {
71 246
        $startDate = datepoint($startDate);
72 246
        $endDate = datepoint($endDate);
73 246
        if ($startDate > $endDate) {
74
            throw new Exception('The ending datepoint must be greater or equal to the starting datepoint');
75
        }
76
        $this->startDate = $startDate;
77
        $this->endDate = $endDate;
78
    }
79
80
    /**
81
     * Returns the Interval starting datepoint.
82 282
     *
83
     * The starting datepoint is included in the specified period.
84 282
     * The starting datepoint is always less than or equal to the ending datepoint.
85 246
     */
86
    public function getStartDate(): DateTimeImmutable
87
    {
88 222
        return $this->startDate;
89 78
    }
90
91
    /**
92 153
     * Returns the Interval ending datepoint.
93
     *
94
     * The ending datepoint is excluded from the specified period.
95
     * The ending datepoint is always greater than or equal to the starting datepoint.
96
     */
97
    public function getEndDate(): DateTimeImmutable
98
    {
99
        return $this->endDate;
100
    }
101
102
    /**
103
     * Returns the Interval duration as expressed in seconds.
104 6
     */
105
    public function getTimestampInterval(): float
106 6
    {
107 3
        return $this->endDate->getTimestamp() - $this->startDate->getTimestamp();
108
    }
109
110 3
    /**
111
     * Returns the Interval duration as a DateInterval object.
112
     */
113
    public function getDateInterval(): DateInterval
114
    {
115
        return $this->startDate->diff($this->endDate);
116
    }
117
118
    /**
119
     * Allows iteration over a set of dates and times,
120
     * recurring at regular intervals, over the instance.
121
122
     *
123
     * @see http://php.net/manual/en/dateperiod.construct.php
124
     */
125
    public function getDatePeriod($duration, int $option = 0): DatePeriod
126
    {
127
        return new DatePeriod($this->startDate, duration($duration), $this->endDate, $option);
128 138
    }
129
130 138
    /**
131
     * Returns the string representation as a ISO8601 interval format.
132 138
     *
133
     * @see https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
134
     *
135
     * @return string
136
     */
137
    public function __toString()
138
    {
139
        $interval = $this->jsonSerialize();
140
141
        return $interval['startDate'].'/'.$interval['endDate'];
142
    }
143
144
    /**
145
     * Returns the Json representation of an instance using
146
     * the JSON representation of dates as returned by Javascript Date.toJSON() method.
147
     *
148
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON
149 177
     * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
150
     *
151 177
     * @return string[]
152 27
     */
153
    public function jsonSerialize()
154
    {
155 165
        static $utc;
156 30
        $utc = $utc ?? new DateTimeZone('UTC');
157
158
        return [
159 153
            'startDate' => $this->startDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
160
            'endDate' => $this->endDate->setTimezone($utc)->format(self::ISO8601_FORMAT),
161
        ];
162
    }
163
164
    /**
165
     * Returns the mathematical represeation of an instance as a right open interval.
166
     *
167
     * @see https://en.wikipedia.org/wiki/Interval_(methematics)#Notations_for_intervals
168
     * @see https://php.net/manual/en/function.date.php for supported format string
169
     * @see https://www.postgresql.org/docs/9.3/static/rangetypes.html
170
     *
171
     * @param string $format the format of the outputted date string
172
     */
173
    public function format(string $format): string
174
    {
175
        return sprintf('[%s, %s)', $this->startDate->format($format), $this->endDate->format($format));
176
    }
177 24
178
    /**
179 24
     * Compares two instances according to their duration.
180
     *
181 24
     * Returns:
182
     * <ul>
183
     * <li> -1 if the current Interval is lesser than the submitted Interval object</li>
184
     * <li>  1 if the current Interval is greater than the submitted Interval object</li>
185
     * <li>  0 if both Interval objects have the same duration</li>
186
     * </ul>
187
     */
188
    public function durationCompare(self $interval): int
189
    {
190
        return $this->endDate <=> $this->startDate->add($interval->getDateInterval());
191 18
    }
192
193 18
    /**
194 15
     * Tells whether the current instance duration is equal to the submitted one.
195
     */
196 15
    public function durationEquals(self $interval): bool
197
    {
198
        return 0 === $this->durationCompare($interval);
199 6
    }
200
201 6
    /**
202
     * Tells whether the current instance duration is greater than the submitted one.
203
     */
204
    public function durationGreaterThan(self $interval): bool
205
    {
206
        return 1 === $this->durationCompare($interval);
207
    }
208
209
    /**
210
     * Tells whether the current instance duration is less than the submitted one.
211
     */
212
    public function durationLessThan(self $interval): bool
213 21
    {
214
        return -1 === $this->durationCompare($interval);
215 21
    }
216
217
    /**
218
     * Tells whether two intervals share the same datepoints.
219
     *
220
     * [--------------------)
221
     * [--------------------)
222
     */
223
    public function equals(self $interval): bool
224
    {
225
        return $this->startDate == $interval->startDate
226 12
            && $this->endDate == $interval->endDate;
227
    }
228 12
229 3
    /**
230 3
     * Tells whether two intervals abuts.
231 3
     *
232
     * [--------------------)
233 3
     *                      [--------------------)
234
     * or
235
     *                      [--------------------)
236 12
     * [--------------------)
237 6
     */
238
    public function abuts(self $interval): bool
239 6
    {
240
        return $this->startDate == $interval->endDate
241
            || $this->endDate == $interval->startDate;
242
    }
243
244
    /**
245
     * Tells whether two intervals overlaps.
246
     *
247
     * [--------------------)
248
     *          [--------------------)
249
     */
250
    public function overlaps(self $interval): bool
251
    {
252
        return $this->startDate < $interval->endDate
253 75
            && $this->endDate > $interval->startDate;
254
    }
255 75
256 75
    /**
257 51
     * Tells whether an interval is entirely after the specified index.
258
     * The index can be a DateTimeInterface object or another Period object.
259
     *
260 24
     *                          [--------------------)
261
     * [--------------------)
262
     */
263
    public function isAfter($index): bool
264
    {
265
        if ($index instanceof self) {
266
            return $this->startDate >= $index->endDate;
267
        }
268
269
        return $this->startDate > datepoint($index);
270
    }
271 12
272
    /**
273 12
     * Tells whether a Interval is entirely before the specified index.
274 3
     * The index can be a DateTimeInterface object or another Period object.
275 3
     *
276 3
     * [--------------------)
277
     *                          [--------------------)
278 3
     */
279
    public function isBefore($index): bool
280
    {
281 12
        if ($index instanceof self) {
282 6
            return $this->endDate <= $index->startDate;
283
        }
284 6
285
        return $this->endDate <= datepoint($index);
286
    }
287
288
    /**
289
     * Tells whether the specified index is fully contained within
290
     * the current Period object.
291
     */
292
    public function contains($index): bool
293
    {
294
        if ($index instanceof self) {
295 39
            return $this->containsPeriod($index);
296
        }
297 39
298 6
        return $this->containsDatePoint(datepoint($index));
299
    }
300 6
301
    /**
302
     * Tells whether the an interval is fully contained within the current instance.
303 36
     *
304
     * [--------------------)
305 30
     *     [----------)
306
     */
307
    private function containsPeriod(self $interval): bool
308
    {
309
        return $this->containsDatePoint($interval->startDate)
310
            && ($interval->endDate >= $this->startDate && $interval->endDate <= $this->endDate);
311
    }
312
313
    /**
314
     * Tells whether a datepoint is fully contained within the current instance.
315
     *
316 24
     * [------|------------)
317
     */
318 24
    private function containsDatePoint(DateTimeInterface $datepoint): bool
319 3
    {
320 3
        return $datepoint >= $this->startDate && $datepoint < $this->endDate;
321 3
    }
322 3
323
    /**
324
     * Allows splitting an instance in smaller Period objects according to a given interval.
325 3
     *
326
     * The returned iterable Interval set is ordered so that:
327
     * <ul>
328 24
     * <li>The first returned object MUST share the starting datepoint of the parent object.</li>
329 24
     * <li>The last returned object MUST share the ending datepoint of the parent object.</li>
330 18
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
331
     * <li>All returned objects except for the first one MUST start immediately after the previously returned object</li>
332
     * </ul>
333 18
     *
334
     * @param DateInterval|Period|string|int $duration
335
     *
336
     * @return Period[]
337
     */
338
    public function split($duration): iterable
339
    {
340
        $startDate = $this->startDate;
341
        $duration = duration($duration);
342
        do {
343
            $endDate = $startDate->add($duration);
344
            if ($endDate > $this->endDate) {
345
                $endDate = $this->endDate;
346 3
            }
347
            yield new self($startDate, $endDate);
348 3
349
            $startDate = $endDate;
350 3
        } while ($startDate < $this->endDate);
351
    }
352
353
    /**
354
     * Allows splitting an instance in smaller Period objects according to a given interval.
355
     *
356
     * The returned iterable Period set is ordered so that:
357
     * <ul>
358
     * <li>The first returned object MUST share the ending datepoint of the parent object.</li>
359
     * <li>The last returned object MUST share the starting datepoint of the parent object.</li>
360
     * <li>The last returned object MUST have a duration equal or lesser than the submitted interval.</li>
361
     * <li>All returned objects except for the first one MUST end immediately before the previously returned object</li>
362
     * </ul>
363 3
     *
364
     * @param DateInterval|Period|string|int $duration
365 3
     *
366
     * @return Period[]
367 3
     */
368
    public function splitBackwards($duration): iterable
369
    {
370
        $endDate = $this->endDate;
371
        $duration = duration($duration);
372
        do {
373
            $startDate = $endDate->sub($duration);
374
            if ($startDate < $this->startDate) {
375
                $startDate = $this->startDate;
376
            }
377
            yield new self($startDate, $endDate);
378
379
            $endDate = $startDate;
380 3
        } while ($endDate > $this->startDate);
381
    }
382 3
383
    /**
384 3
     * Computes the intersection between two instances.
385
     *
386
     * [--------------------)
387
     *          ∩
388
     *                 [----------)
389
     *          =
390
     *                 [----)
391
     *
392
     * @throws Exception If both objects do not overlaps
393
     */
394
    public function intersect(self $interval): self
395
    {
396
        if (!$this->overlaps($interval)) {
397 3
            throw new Exception(sprintf('Both %s objects should overlaps', Period::class));
398
        }
399 3
400
        return new self(
401 3
            ($interval->startDate > $this->startDate) ? $interval->startDate : $this->startDate,
402
            ($interval->endDate < $this->endDate) ? $interval->endDate : $this->endDate
403
        );
404
    }
405
406
    /**
407
     * Computes the difference between two overlapsing instances.
408
     *
409
     * Returns an array containing the difference expressed as Period objects
410
     * The array will always contains 2 elements:
411
     *
412 177
     * <ul>
413
     * <li>an NULL filled array if both objects have the same datepoints</li>
414 177
     * <li>one Period object and NULL if both objects share one datepoint</li>
415
     * <li>two Period objects if both objects share no datepoint</li>
416
     * </ul>
417
     *
418
     * [--------------------)
419
     *          -
420
     *                [-----------)
421
     *          =
422
     * [--------------)  +  [-----)
423
     *
424
     * @throws Exception if both objects do not overlaps
425 162
     */
426
    public function diff(self $interval): array
427 162
    {
428
        if ($interval->equals($this)) {
429
            return [null, null];
430
        }
431
432
        $intersect = $this->intersect($interval);
433
        $merge = $this->merge($interval);
434
        if ($merge->startDate == $intersect->startDate) {
435 18
            return [$merge->startingOn($intersect->endDate), null];
436
        }
437 18
438
        if ($merge->endDate == $intersect->endDate) {
439
            return [$merge->endingOn($intersect->startDate), null];
440
        }
441
442
        return [
443
            $merge->endingOn($intersect->startDate),
444
            $merge->startingOn($intersect->endDate),
445 33
        ];
446
    }
447 33
448
    /**
449
     * Computes the gap between two instances.
450
     *
451
     * [--------------------)
452
     *          +
453
     *                          [----------)
454
     *          =
455
     *                      [---)
456
     *
457
     * @throws Exception If both objects overlaps
458
     */
459
    public function gap(self $interval): self
460
    {
461
        if ($this->overlaps($interval)) {
462
            throw new Exception(sprintf('Both %s objects should not overlaps', self::class));
463
        }
464
465
        if ($interval->startDate > $this->startDate) {
466
            return new self($this->endDate, $interval->startDate);
467
        }
468
469 24
        return new self($interval->endDate, $this->startDate);
470
    }
471 24
472
    /**
473
     * Returns an instance with the specified starting datepoint.
474
     *
475
     * This method MUST retain the state of the current instance, and return
476
     * an instance that contains the specified starting datepoint.
477
     *
478
     * @param DateTimeInterface|int|string $datepoint
479
     */
480
    public function startingOn($datepoint): self
481
    {
482
        $startDate = datepoint($datepoint);
483
        if ($startDate == $this->startDate) {
484
            return $this;
485
        }
486
487
        return new self($startDate, $this->endDate);
488
    }
489
490
    /**
491
     * Returns an instance with the specified ending datepoint.
492
     *
493
     * This method MUST retain the state of the current instance, and return
494
     * an instance that contains the specified ending datepoint.
495
     *
496
     * @param DateTimeInterface|int|string $datepoint
497 12
     */
498
    public function endingOn($datepoint): self
499 12
    {
500 12
        $endDate = datepoint($datepoint);
501
        if ($endDate == $this->endDate) {
502 12
            return $this;
503 12
        }
504 6
505
        return new self($this->startDate, $endDate);
506 12
    }
507
508 9
    /**
509 9
     * Returns a new instance with a new ending datepoint.
510 9
     *
511
     * @param DateInterval|Period|int|string $duration
512
     */
513
    public function withDurationAfterStart($duration): self
514
    {
515
        return $this->endingOn($this->startDate->add(duration($duration)));
516
    }
517
518
    /**
519
     * Returns a new instance with a new starting datepoint.
520
     *
521
     * @param DateInterval|Period|int|string $duration
522
     */
523
    public function withDurationBeforeEnd($duration): self
524
    {
525
        return $this->startingOn($this->endDate->sub(duration($duration)));
526
    }
527
528
    /**
529
     * Returns a new instance with a new starting datepoint
530
     * moved forward or backward by the given interval.
531
     *
532
     * @param DateInterval|Period|int|string $duration
533
     */
534
    public function moveStartDate($duration): self
535 6
    {
536
        return $this->startingOn($this->startDate->add(duration($duration)));
537 6
    }
538 6
539
    /**
540 6
     * Returns a new instance with a new ending datepoint
541 6
     * moved forward or backward by the given interval.
542 3
     *
543
     * @param DateInterval|Period|int|string $duration
544 6
     */
545
    public function moveEndDate($duration): self
546 6
    {
547 6
        return $this->endingOn($this->endDate->add(duration($duration)));
548 6
    }
549
550
    /**
551
     * Returns a new instance where the datepoints
552
     * are moved forwards or backward simultaneously by the given DateInterval.
553
     *
554
     * This method MUST retain the state of the current instance, and return
555
     * an instance that contains the specified new datepoints.
556
     *
557
     * @param DateInterval|Period|int|string $duration
558 3
     */
559
    public function move($duration): self
560 3
    {
561
        $duration = duration($duration);
562 3
        $interval = new self($this->startDate->add($duration), $this->endDate->add($duration));
563
        if ($interval->equals($this)) {
564
            return $this;
565
        }
566
567
        return $interval;
568
    }
569
570
    /**
571
     * Returns an instance where the given DateInterval is simultaneously
572
     * substracted from the starting datepoint and added to the ending datepoint.
573 6
     *
574
     * This method MUST retain the state of the current instance, and return
575 6
     * an instance that contains the specified new datepoints.
576 6
     *
577 6
     * Depending on the duration value, the resulting instance duration will be expanded or shrinked.
578
     *
579
     * @param DateInterval|Period|int|string $duration
580 6
     */
581 6
    public function expand($duration): self
582
    {
583
        $duration = duration($duration);
584
        $interval = new self($this->startDate->sub($duration), $this->endDate->add($duration));
585
        if ($interval->equals($this)) {
586
            return $this;
587
        }
588
589
        return $interval;
590
    }
591
592
    /**
593
     * Returns the difference between two instances expressed in seconds.
594
     */
595
    public function timestampIntervalDiff(self $interval): float
596
    {
597
        return $this->getTimestampInterval() - $interval->getTimestampInterval();
598
    }
599 21
600
    /**
601 21
     * Returns the difference between two instances expressed in DateInterval.
602
     */
603
    public function dateIntervalDiff(self $interval): DateInterval
604
    {
605
        return $this->endDate->diff($this->startDate->add($interval->getDateInterval()));
606
    }
607
608
    /**
609
     * Merges one or more instances to return a new instance.
610
     * The resulting instance represents the largest duration possible.
611
     *
612 9
     * @param Period ...$intervals
613
     */
614 9
    public function merge(self $interval, self ...$intervals): self
615
    {
616
        array_unshift($intervals, $interval);
617
        $carry = $this;
618
        foreach ($intervals as $interval) {
619
            if ($carry->startDate > $interval->startDate) {
620
                $carry = $carry->startingOn($interval->startDate);
621
            }
622
623
            if ($carry->endDate < $interval->endDate) {
624
                $carry = $carry->endingOn($interval->endDate);
625 9
            }
626
        }
627 9
628
        return $carry;
629
    }
630
}
631