Completed
Push — master ( d7399a...990b84 )
by ignace nyamagana
21:09 queued 06:10
created

Period::mergeOne()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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