GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#50)
by Jérôme
01:04
created

Period::asDatePeriod()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Spatie\Period;
4
5
use DateTime;
6
use DatePeriod;
7
use DateInterval;
8
use DateTimeImmutable;
9
use DateTimeInterface;
10
use IteratorAggregate;
11
use Spatie\Period\Exceptions\InvalidDate;
12
use Spatie\Period\Exceptions\InvalidPeriod;
13
use Spatie\Period\Exceptions\CannotComparePeriods;
14
15
class Period implements IteratorAggregate
16
{
17
    public const DEFAULT_PRECISION = Precision::DAY;
18
19
    /** @var \DateTimeImmutable */
20
    protected $start;
21
22
    /** @var \DateTimeImmutable */
23
    protected $end;
24
25
    /** @var \DateInterval */
26
    protected $interval;
27
28
    /** @var \DateTimeImmutable */
29
    private $includedStart;
30
31
    /** @var \DateTimeImmutable */
32
    private $includedEnd;
33
34
    /** @var int */
35
    private $boundaryExclusionMask;
36
37
    /** @var int */
38
    private $precisionMask;
39
40
    public function __construct(
41
        DateTimeImmutable $start,
42
        DateTimeImmutable $end,
43
        ?int $precisionMask = null,
44
        ?int $boundaryExclusionMask = null
45
    ) {
46
        if ($start > $end) {
47
            throw InvalidPeriod::endBeforeStart($start, $end);
48
        }
49
50
        $this->boundaryExclusionMask = $boundaryExclusionMask ?? Boundaries::EXCLUDE_NONE;
51
        $this->precisionMask = $precisionMask ?? static::DEFAULT_PRECISION;
52
53
        $this->start = $this->roundDate($start, $this->precisionMask);
54
        $this->end = $this->roundDate($end, $this->precisionMask);
55
        $this->interval = $this->createDateInterval($this->precisionMask);
56
57
        $this->includedStart = $this->startIncluded()
58
            ? $this->start
59
            : $this->start->add($this->interval);
60
61
        $this->includedEnd = $this->endIncluded()
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->endIncluded() ? $...d->sub($this->interval) can also be of type false. However, the property $includedEnd is declared as type object<DateTimeImmutable>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
62
            ? $this->end
63
            : $this->end->sub($this->interval);
64
    }
65
66
    /**
67
     * @param string|DateTimeInterface $start
68
     * @param string|DateTimeInterface $end
69
     * @param int|null $precisionMask
70
     * @param int|null $boundaryExclusionMask
71
     * @param string|null $format
72
     *
73
     * @return static
74
     */
75
    public static function make(
76
        $start,
77
        $end,
78
        ?int $precisionMask = null,
79
        ?int $boundaryExclusionMask = null,
80
        ?string $format = null
81
    ): Period {
82
        if ($start === null) {
83
            throw InvalidDate::cannotBeNull('Start date');
84
        }
85
86
        if ($end === null) {
87
            throw InvalidDate::cannotBeNull('End date');
88
        }
89
90
        return new static(
91
            static::resolveDate($start, $format),
92
            static::resolveDate($end, $format),
93
            $precisionMask,
94
            $boundaryExclusionMask
95
        );
96
    }
97
98
    public static function fromDatePeriod(DatePeriod $period): Period
99
    {
100
        $start = $period->getStartDate();
101
        $end = $period->getEndDate();
102
        $precision = static::convertDateIntervalToPrecision($period->getDateInterval());
103
        $boundaries = $period->include_start_date ? Boundaries::EXCLUDE_NONE : Boundaries::EXCLUDE_START;
0 ignored issues
show
Bug introduced by
The property include_start_date does not seem to exist in DatePeriod.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
104
105
        return static::make($start, $end, $precision, $boundaries);
106
    }
107
108
    public function startIncluded(): bool
109
    {
110
        return ! $this->startExcluded();
111
    }
112
113
    public function startExcluded(): bool
114
    {
115
        return Boundaries::EXCLUDE_START & $this->boundaryExclusionMask;
116
    }
117
118
    public function endIncluded(): bool
119
    {
120
        return ! $this->endExcluded();
121
    }
122
123
    public function endExcluded(): bool
124
    {
125
        return Boundaries::EXCLUDE_END & $this->boundaryExclusionMask;
126
    }
127
128
    public function getStart(): DateTimeImmutable
129
    {
130
        return $this->start;
131
    }
132
133
    public function getIncludedStart(): DateTimeImmutable
134
    {
135
        return $this->includedStart;
136
    }
137
138
    public function getEnd(): DateTimeImmutable
139
    {
140
        return $this->end;
141
    }
142
143
    public function getIncludedEnd(): DateTimeImmutable
144
    {
145
        return $this->includedEnd;
146
    }
147
148
    public function length(): int
149
    {
150
        $length = $this->getIncludedStart()->diff($this->getIncludedEnd())->days + 1;
151
152
        return $length;
153
    }
154
155
    public function overlapsWith(Period $period): bool
156
    {
157
        $this->ensurePrecisionMatches($period);
158
159
        if ($this->getIncludedStart() > $period->getIncludedEnd()) {
160
            return false;
161
        }
162
163
        if ($period->getIncludedStart() > $this->getIncludedEnd()) {
164
            return false;
165
        }
166
167
        return true;
168
    }
169
170
    public function touchesWith(Period $period): bool
171
    {
172
        $this->ensurePrecisionMatches($period);
173
174
        if ($this->getIncludedEnd()->diff($period->getIncludedStart())->days <= 1) {
175
            return true;
176
        }
177
178
        if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) {
179
            return true;
180
        }
181
182
        return false;
183
    }
184
185
    public function startsBefore(DateTimeInterface $date): bool
186
    {
187
        return $this->getIncludedStart() < $date;
188
    }
189
190
    public function startsBeforeOrAt(DateTimeInterface $date): bool
191
    {
192
        return $this->getIncludedStart() <= $date;
193
    }
194
195
    public function startsAfter(DateTimeInterface $date): bool
196
    {
197
        return $this->getIncludedStart() > $date;
198
    }
199
200
    public function startsAfterOrAt(DateTimeInterface $date): bool
201
    {
202
        return $this->getIncludedStart() >= $date;
203
    }
204
205
    public function startsAt(DateTimeInterface $date): bool
206
    {
207
        return $this->getIncludedStart()->getTimestamp() === $this->roundDate(
208
            $date,
209
            $this->precisionMask
210
        )->getTimestamp();
211
    }
212
213
    public function endsBefore(DateTimeInterface $date): bool
214
    {
215
        return $this->getIncludedEnd() < $this->roundDate(
216
                $date,
217
                $this->precisionMask
218
            );
219
    }
220
221
    public function endsBeforeOrAt(DateTimeInterface $date): bool
222
    {
223
        return $this->getIncludedEnd() <= $this->roundDate(
224
                $date,
225
                $this->precisionMask
226
            );
227
    }
228
229
    public function endsAfter(DateTimeInterface $date): bool
230
    {
231
        return $this->getIncludedEnd() > $this->roundDate(
232
                $date,
233
                $this->precisionMask
234
            );
235
    }
236
237
    public function endsAfterOrAt(DateTimeInterface $date): bool
238
    {
239
        return $this->getIncludedEnd() >= $this->roundDate(
240
                $date,
241
                $this->precisionMask
242
            );
243
    }
244
245
    public function endsAt(DateTimeInterface $date): bool
246
    {
247
        return $this->getIncludedEnd()->getTimestamp() === $this->roundDate(
248
                $date,
249
                $this->precisionMask
250
            )->getTimestamp();
251
    }
252
253
    public function contains(DateTimeInterface $date): bool
254
    {
255
        if ($this->roundDate($date, $this->precisionMask) < $this->getIncludedStart()) {
256
            return false;
257
        }
258
259
        if ($this->roundDate($date, $this->precisionMask) > $this->getIncludedEnd()) {
260
            return false;
261
        }
262
263
        return true;
264
    }
265
266
    public function equals(Period $period): bool
267
    {
268
        $this->ensurePrecisionMatches($period);
269
270
        if ($period->getIncludedStart()->getTimestamp() !== $this->getIncludedStart()->getTimestamp()) {
271
            return false;
272
        }
273
274
        if ($period->getIncludedEnd()->getTimestamp() !== $this->getIncludedEnd()->getTimestamp()) {
275
            return false;
276
        }
277
278
        return true;
279
    }
280
281
    /**
282
     * @param \Spatie\Period\Period $period
283
     *
284
     * @return static|null
285
     * @throws \Exception
286
     */
287
    public function gap(Period $period): ?Period
288
    {
289
        $this->ensurePrecisionMatches($period);
290
291
        if ($this->overlapsWith($period)) {
292
            return null;
293
        }
294
295
        if ($this->touchesWith($period)) {
296
            return null;
297
        }
298
299
        if ($this->getIncludedStart() >= $period->getIncludedEnd()) {
300
            return static::make(
301
                $period->getIncludedEnd()->add($this->interval),
302
                $this->getIncludedStart()->sub($this->interval),
0 ignored issues
show
Security Bug introduced by
It seems like $this->getIncludedStart()->sub($this->interval) targeting DateTimeImmutable::sub() can also be of type false; however, Spatie\Period\Period::make() does only seem to accept string|object<DateTimeInterface>, did you maybe forget to handle an error condition?
Loading history...
303
                $this->getPrecisionMask()
304
            );
305
        }
306
307
        return static::make(
308
            $this->getIncludedEnd()->add($this->interval),
309
            $period->getIncludedStart()->sub($this->interval),
0 ignored issues
show
Security Bug introduced by
It seems like $period->getIncludedStart()->sub($this->interval) targeting DateTimeImmutable::sub() can also be of type false; however, Spatie\Period\Period::make() does only seem to accept string|object<DateTimeInterface>, did you maybe forget to handle an error condition?
Loading history...
310
            $this->getPrecisionMask()
311
        );
312
    }
313
314
    /**
315
     * @param \Spatie\Period\Period $period
316
     *
317
     * @return static|null
318
     */
319
    public function overlapSingle(Period $period): ?Period
320
    {
321
        $this->ensurePrecisionMatches($period);
322
323
        $start = $this->getIncludedStart() > $period->getIncludedStart()
324
            ? $this->getIncludedStart()
325
            : $period->getIncludedStart();
326
327
        $end = $this->getIncludedEnd() < $period->getIncludedEnd()
328
            ? $this->getIncludedEnd()
329
            : $period->getIncludedEnd();
330
331
        if ($start > $end) {
332
            return null;
333
        }
334
335
        return static::make($start, $end, $this->getPrecisionMask());
336
    }
337
338
    /**
339
     * @param \Spatie\Period\Period ...$periods
340
     *
341
     * @return \Spatie\Period\PeriodCollection|static[]
342
     */
343
    public function overlap(Period ...$periods): PeriodCollection
344
    {
345
        $overlapCollection = new PeriodCollection();
346
347
        foreach ($periods as $period) {
348
            $overlap = $this->overlapSingle($period);
349
350
            if ($overlap === null) {
351
                continue;
352
            }
353
354
            $overlapCollection[] = $overlap;
355
        }
356
357
        return $overlapCollection;
358
    }
359
360
    /**
361
     * @param \Spatie\Period\Period ...$periods
362
     *
363
     * @return static
364
     */
365
    public function overlapAll(Period ...$periods): Period
366
    {
367
        $overlap = clone $this;
368
369
        if (! count($periods)) {
370
            return $overlap;
371
        }
372
373
        foreach ($periods as $period) {
374
            $overlap = $overlap->overlapSingle($period);
375
        }
376
377
        return $overlap;
378
    }
379
380
    public function diffSingle(Period $period): PeriodCollection
381
    {
382
        $this->ensurePrecisionMatches($period);
383
384
        $periodCollection = new PeriodCollection();
385
386
        if (! $this->overlapsWith($period)) {
387
            $periodCollection[] = clone $this;
388
            $periodCollection[] = clone $period;
389
390
            return $periodCollection;
391
        }
392
393
        $overlap = $this->overlapSingle($period);
394
395
        $start = $this->getIncludedStart() < $period->getIncludedStart()
396
            ? $this->getIncludedStart()
397
            : $period->getIncludedStart();
398
399
        $end = $this->getIncludedEnd() > $period->getIncludedEnd()
400
            ? $this->getIncludedEnd()
401
            : $period->getIncludedEnd();
402
403
        if ($overlap->getIncludedStart() > $start) {
404
            $periodCollection[] = static::make(
405
                $start,
406
                $overlap->getIncludedStart()->sub($this->interval),
407
                $this->getPrecisionMask()
408
            );
409
        }
410
411
        if ($overlap->getIncludedEnd() < $end) {
412
            $periodCollection[] = static::make(
413
                $overlap->getIncludedEnd()->add($this->interval),
414
                $end,
415
                $this->getPrecisionMask()
416
            );
417
        }
418
419
        return $periodCollection;
420
    }
421
422
    /**
423
     * @param \Spatie\Period\Period ...$periods
424
     *
425
     * @return \Spatie\Period\PeriodCollection|static[]
426
     */
427
    public function diff(Period ...$periods): PeriodCollection
428
    {
429
        if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) {
430
            $collection = new PeriodCollection();
431
432
            $gap = $this->gap($periods[0]);
433
434
            if ($gap !== null) {
435
                $collection[] = $gap;
436
            }
437
438
            return $collection;
439
        }
440
441
        $diffs = [];
442
443
        foreach ($periods as $period) {
444
            $diffs[] = $this->diffSingle($period);
445
        }
446
447
        $collection = (new PeriodCollection($this))->overlap(...$diffs);
448
449
        return $collection;
450
    }
451
452
    public function getPrecisionMask(): int
453
    {
454
        return $this->precisionMask;
455
    }
456
457
    public function asDatePeriod(): DatePeriod
458
    {
459
        return new DatePeriod(
460
            $this->getIncludedStart(),
461
            $this->interval,
462
            $this->getIncludedEnd()
463
        );
464
    }
465
466
    public function getIterator()
467
    {
468
        return new DatePeriod(
469
            $this->getIncludedStart(),
470
            $this->interval,
471
            $this->getIncludedEnd()->add($this->interval)
472
        );
473
    }
474
475
    protected static function resolveDate($date, ?string $format): DateTimeImmutable
476
    {
477
        if ($date instanceof DateTimeImmutable) {
478
            return $date;
479
        }
480
481
        if ($date instanceof DateTime) {
482
            return DateTimeImmutable::createFromMutable($date);
483
        }
484
485
        $format = static::resolveFormat($date, $format);
486
487
        if (! is_string($date)) {
488
            throw InvalidDate::forFormat($date, $format);
489
        }
490
491
        $dateTime = DateTimeImmutable::createFromFormat($format, $date);
492
493
        if ($dateTime === false) {
494
            throw InvalidDate::forFormat($date, $format);
495
        }
496
497
        if (strpos($format, ' ') === false) {
498
            $dateTime = $dateTime->setTime(0, 0, 0);
499
        }
500
501
        return $dateTime;
502
    }
503
504
    protected static function resolveFormat($date, ?string $format): string
505
    {
506
        if ($format !== null) {
507
            return $format;
508
        }
509
510
        if (strpos($format, ' ') === false && strpos($date, ' ') !== false) {
511
            return 'Y-m-d H:i:s';
512
        }
513
514
        return 'Y-m-d';
515
    }
516
517
    protected function roundDate(DateTimeInterface $date, int $precision): DateTimeImmutable
518
    {
519
        [$year, $month, $day, $hour, $minute, $second] = explode(' ', $date->format('Y m d H i s'));
0 ignored issues
show
Bug introduced by
The variable $year does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $month seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $day seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $hour seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $minute seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
Bug introduced by
The variable $second seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
520
521
        $month = (Precision::MONTH & $precision) === Precision::MONTH ? $month : '01';
0 ignored issues
show
Bug introduced by
The variable $month seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
522
        $day = (Precision::DAY & $precision) === Precision::DAY ? $day : '01';
0 ignored issues
show
Bug introduced by
The variable $day seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
523
        $hour = (Precision::HOUR & $precision) === Precision::HOUR ? $hour : '00';
0 ignored issues
show
Bug introduced by
The variable $hour seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
524
        $minute = (Precision::MINUTE & $precision) === Precision::MINUTE ? $minute : '00';
0 ignored issues
show
Bug introduced by
The variable $minute seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
525
        $second = (Precision::SECOND & $precision) === Precision::SECOND ? $second : '00';
0 ignored issues
show
Bug introduced by
The variable $second seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
526
527
        return DateTimeImmutable::createFromFormat(
528
            'Y m d H i s',
529
            implode(' ', [$year, $month, $day, $hour, $minute, $second])
530
        );
531
    }
532
533
    protected static function convertDateIntervalToPrecision(DateInterval $interval): int
534
    {
535
        $now = new DateTimeImmutable();
536
        $target = $now->add($interval);
537
538
        if ($target == $now->add(new DateInterval('PT1S'))) {
539
            return Precision::SECOND;
540
        }
541
        if ($target == $now->add(new DateInterval('PT1M'))) {
542
            return Precision::MINUTE;
543
        }
544
545
        if ($target == $now->add(new DateInterval('PT1H'))) {
546
            return Precision::HOUR;
547
        }
548
549
        if ($target == $now->add(new DateInterval('P1D'))) {
550
            return Precision::DAY;
551
        }
552
553
        if ($target == $now->add(new DateInterval('P1M'))) {
554
            return Precision::MONTH;
555
        }
556
557
        if ($target == $now->add(new DateInterval('P1Y'))) {
558
            return Precision::YEAR;
559
        }
560
561
        return static::DEFAULT_PRECISION;
562
    }
563
564
    protected function createDateInterval(int $precision): DateInterval
565
    {
566
        $interval = [
567
            Precision::SECOND => 'PT1S',
568
            Precision::MINUTE => 'PT1M',
569
            Precision::HOUR => 'PT1H',
570
            Precision::DAY => 'P1D',
571
            Precision::MONTH => 'P1M',
572
            Precision::YEAR => 'P1Y',
573
        ][$precision];
574
575
        return new DateInterval($interval);
576
    }
577
578
    protected function ensurePrecisionMatches(Period $period): void
579
    {
580
        if ($this->precisionMask === $period->precisionMask) {
581
            return;
582
        }
583
584
        throw CannotComparePeriods::precisionDoesNotMatch();
585
    }
586
}
587