|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Spatie\Period; |
|
4
|
|
|
|
|
5
|
|
|
use DateTime; |
|
6
|
|
|
use DateInterval; |
|
7
|
|
|
use DateTimeImmutable; |
|
8
|
|
|
use DateTimeInterface; |
|
9
|
|
|
use Spatie\Period\Exceptions\InvalidDate; |
|
10
|
|
|
use Spatie\Period\Exceptions\InvalidPeriod; |
|
11
|
|
|
|
|
12
|
|
|
class Period |
|
13
|
|
|
{ |
|
14
|
|
|
const EXCLUDE_NONE = 0; |
|
15
|
|
|
const EXCLUDE_START = 2; |
|
16
|
|
|
const EXCLUDE_END = 4; |
|
17
|
|
|
const EXCLUDE_ALL = 6; |
|
18
|
|
|
|
|
19
|
|
|
/** @var \DateTimeImmutable */ |
|
20
|
|
|
protected $start; |
|
21
|
|
|
|
|
22
|
|
|
/** @var \DateTimeImmutable */ |
|
23
|
|
|
protected $end; |
|
24
|
|
|
|
|
25
|
|
|
/** @var \DateInterval */ |
|
26
|
|
|
protected $interval; |
|
27
|
|
|
|
|
28
|
|
|
/** @var int */ |
|
29
|
|
|
private $exclusionMask; |
|
30
|
|
|
|
|
31
|
|
|
/** @var \DateTimeImmutable */ |
|
32
|
|
|
private $includedStart; |
|
33
|
|
|
|
|
34
|
|
|
/** @var \DateTimeImmutable */ |
|
35
|
|
|
private $includedEnd; |
|
36
|
|
|
|
|
37
|
|
|
public function __construct( |
|
38
|
|
|
DateTimeImmutable $start, |
|
39
|
|
|
DateTimeImmutable $end, |
|
40
|
|
|
int $exclusionMask = 0 |
|
41
|
|
|
) { |
|
42
|
|
|
if ($start > $end) { |
|
43
|
|
|
throw InvalidPeriod::endBeforeStart($start, $end); |
|
44
|
|
|
} |
|
45
|
|
|
|
|
46
|
|
|
$this->start = $start; |
|
47
|
|
|
$this->end = $end; |
|
48
|
|
|
$this->exclusionMask = $exclusionMask; |
|
49
|
|
|
$this->interval = new DateInterval('P1D'); |
|
50
|
|
|
|
|
51
|
|
|
$this->includedStart = $this->startIncluded() |
|
52
|
|
|
? $this->start |
|
53
|
|
|
: $this->start->add($this->interval); |
|
54
|
|
|
|
|
55
|
|
|
$this->includedEnd = $this->endIncluded() |
|
|
|
|
|
|
56
|
|
|
? $this->end |
|
57
|
|
|
: $this->end->sub($this->interval); |
|
58
|
|
|
} |
|
59
|
|
|
|
|
60
|
|
|
/** |
|
61
|
|
|
* @param \DateTimeInterface|string $start |
|
62
|
|
|
* @param \DateTimeInterface|string $end |
|
63
|
|
|
* @param string|null $format |
|
64
|
|
|
* |
|
65
|
|
|
* @return \Spatie\Period\Period|static |
|
66
|
|
|
*/ |
|
67
|
|
|
public static function make( |
|
68
|
|
|
$start, |
|
69
|
|
|
$end, |
|
70
|
|
|
?string $format = null, |
|
71
|
|
|
int $exclusionMask = 0 |
|
72
|
|
|
): Period { |
|
73
|
|
|
if ($start === null) { |
|
74
|
|
|
throw InvalidDate::cannotBeNull('Start date'); |
|
75
|
|
|
} |
|
76
|
|
|
|
|
77
|
|
|
if ($end === null) { |
|
78
|
|
|
throw InvalidDate::cannotBeNull('End date'); |
|
79
|
|
|
} |
|
80
|
|
|
|
|
81
|
|
|
return new static( |
|
82
|
|
|
self::resolveDate($start, $format), |
|
83
|
|
|
self::resolveDate($end, $format), |
|
84
|
|
|
$exclusionMask |
|
85
|
|
|
); |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
public function startIncluded(): bool |
|
89
|
|
|
{ |
|
90
|
|
|
return ! $this->startExcluded(); |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
public function startExcluded(): bool |
|
94
|
|
|
{ |
|
95
|
|
|
return self::EXCLUDE_START & $this->exclusionMask; |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
public function endIncluded(): bool |
|
99
|
|
|
{ |
|
100
|
|
|
return ! $this->endExcluded(); |
|
101
|
|
|
} |
|
102
|
|
|
|
|
103
|
|
|
public function endExcluded(): bool |
|
104
|
|
|
{ |
|
105
|
|
|
return self::EXCLUDE_END & $this->exclusionMask; |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
protected static function resolveDate($date, ?string $format): DateTimeImmutable |
|
109
|
|
|
{ |
|
110
|
|
|
if ($date instanceof DateTimeImmutable) { |
|
111
|
|
|
return $date; |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
if ($date instanceof DateTime) { |
|
115
|
|
|
return DateTimeImmutable::createFromMutable($date); |
|
116
|
|
|
} |
|
117
|
|
|
|
|
118
|
|
|
$format = self::resolveFormat($date, $format); |
|
119
|
|
|
|
|
120
|
|
|
if (! is_string($date)) { |
|
121
|
|
|
throw InvalidDate::forFormat($date, $format); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
$dateTime = DateTimeImmutable::createFromFormat($format, $date); |
|
125
|
|
|
|
|
126
|
|
|
if ($dateTime === false) { |
|
127
|
|
|
throw InvalidDate::forFormat($date, $format); |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
|
|
if (strpos($format, ' ') === false) { |
|
131
|
|
|
$dateTime = $dateTime->setTime(0, 0, 0); |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
|
|
return $dateTime; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
protected static function resolveFormat($date, ?string $format): string |
|
138
|
|
|
{ |
|
139
|
|
|
if ($format !== null) { |
|
140
|
|
|
return $format; |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
if ( |
|
144
|
|
|
strpos($format, ' ') === false |
|
145
|
|
|
&& strpos($date, ' ') !== false |
|
146
|
|
|
) { |
|
147
|
|
|
return 'Y-m-d H:i:s'; |
|
148
|
|
|
} |
|
149
|
|
|
|
|
150
|
|
|
return 'Y-m-d'; |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
public function getStart(): DateTimeImmutable |
|
154
|
|
|
{ |
|
155
|
|
|
return $this->start; |
|
156
|
|
|
} |
|
157
|
|
|
|
|
158
|
|
|
public function getIncludedStart(): DateTimeImmutable |
|
159
|
|
|
{ |
|
160
|
|
|
return $this->includedStart; |
|
161
|
|
|
} |
|
162
|
|
|
|
|
163
|
|
|
public function getEnd(): DateTimeImmutable |
|
164
|
|
|
{ |
|
165
|
|
|
return $this->end; |
|
166
|
|
|
} |
|
167
|
|
|
|
|
168
|
|
|
public function getIncludedEnd(): DateTimeImmutable |
|
169
|
|
|
{ |
|
170
|
|
|
return $this->includedEnd; |
|
171
|
|
|
} |
|
172
|
|
|
|
|
173
|
|
|
public function length(): int |
|
174
|
|
|
{ |
|
175
|
|
|
$length = $this->getIncludedStart()->diff($this->getIncludedEnd())->days + 1; |
|
176
|
|
|
|
|
177
|
|
|
return $length; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
public function overlapsWith(Period $period): bool |
|
181
|
|
|
{ |
|
182
|
|
|
if ($this->getIncludedStart() > $period->getIncludedEnd()) { |
|
183
|
|
|
return false; |
|
184
|
|
|
} |
|
185
|
|
|
|
|
186
|
|
|
if ($period->getIncludedStart() > $this->getIncludedEnd()) { |
|
187
|
|
|
return false; |
|
188
|
|
|
} |
|
189
|
|
|
|
|
190
|
|
|
return true; |
|
191
|
|
|
} |
|
192
|
|
|
|
|
193
|
|
|
public function touchesWith(Period $period): bool |
|
194
|
|
|
{ |
|
195
|
|
|
if ($this->getIncludedEnd()->diff($period->getIncludedStart())->days <= 1) { |
|
196
|
|
|
return true; |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
if ($this->getIncludedStart()->diff($period->getIncludedEnd())->days <= 1) { |
|
200
|
|
|
return true; |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
return false; |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
public function startsAfterOrAt(DateTimeInterface $date): bool |
|
207
|
|
|
{ |
|
208
|
|
|
return $this->getIncludedStart() >= $date; |
|
209
|
|
|
} |
|
210
|
|
|
|
|
211
|
|
|
public function endsAfterOrAt(DateTimeInterface $date): bool |
|
212
|
|
|
{ |
|
213
|
|
|
return $this->end >= $date; |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
public function startsBeforeOrAt(DateTimeInterface $date): bool |
|
217
|
|
|
{ |
|
218
|
|
|
return $this->getIncludedStart() <= $date; |
|
219
|
|
|
} |
|
220
|
|
|
|
|
221
|
|
|
public function endsBeforeOrAt(DateTimeInterface $date): bool |
|
222
|
|
|
{ |
|
223
|
|
|
return $this->end <= $date; |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
public function startsAfter(DateTimeInterface $date): bool |
|
227
|
|
|
{ |
|
228
|
|
|
return $this->getIncludedStart() > $date; |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
public function endsAfter(DateTimeInterface $date): bool |
|
232
|
|
|
{ |
|
233
|
|
|
return $this->end > $date; |
|
234
|
|
|
} |
|
235
|
|
|
|
|
236
|
|
|
public function startsBefore(DateTimeInterface $date): bool |
|
237
|
|
|
{ |
|
238
|
|
|
return $this->getIncludedStart() < $date; |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
public function endsBefore(DateTimeInterface $date): bool |
|
242
|
|
|
{ |
|
243
|
|
|
return $this->getIncludedEnd() < $date; |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
public function contains(DateTimeInterface $date): bool |
|
247
|
|
|
{ |
|
248
|
|
|
if ($date < $this->getIncludedStart()) { |
|
249
|
|
|
return false; |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
if ($date > $this->getIncludedEnd()) { |
|
253
|
|
|
return false; |
|
254
|
|
|
} |
|
255
|
|
|
|
|
256
|
|
|
return true; |
|
257
|
|
|
} |
|
258
|
|
|
|
|
259
|
|
|
public function equals(Period $period): bool |
|
260
|
|
|
{ |
|
261
|
|
|
if ($period->getIncludedStart()->getTimestamp() !== $this->getIncludedStart()->getTimestamp()) { |
|
262
|
|
|
return false; |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
if ($period->getIncludedEnd()->getTimestamp() !== $this->getIncludedEnd()->getTimestamp()) { |
|
266
|
|
|
return false; |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
return true; |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
/** |
|
273
|
|
|
* @param \Spatie\Period\Period $period |
|
274
|
|
|
* |
|
275
|
|
|
* @return \Spatie\Period\Period|static|null |
|
276
|
|
|
* @throws \Exception |
|
277
|
|
|
*/ |
|
278
|
|
|
public function gap(Period $period): ?Period |
|
279
|
|
|
{ |
|
280
|
|
|
if ($this->overlapsWith($period)) { |
|
281
|
|
|
return null; |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
if ($this->touchesWith($period)) { |
|
285
|
|
|
return null; |
|
286
|
|
|
} |
|
287
|
|
|
|
|
288
|
|
|
if ($this->getIncludedStart() >= $period->getIncludedEnd()) { |
|
289
|
|
|
return static::make( |
|
290
|
|
|
$period->getIncludedEnd()->add($this->interval), |
|
291
|
|
|
$this->getIncludedStart()->sub($this->interval) |
|
|
|
|
|
|
292
|
|
|
); |
|
293
|
|
|
} |
|
294
|
|
|
|
|
295
|
|
|
return static::make( |
|
296
|
|
|
$this->getIncludedEnd()->add($this->interval), |
|
297
|
|
|
$period->getIncludedStart()->sub($this->interval) |
|
|
|
|
|
|
298
|
|
|
); |
|
299
|
|
|
} |
|
300
|
|
|
|
|
301
|
|
|
/** |
|
302
|
|
|
* @param \Spatie\Period\Period $period |
|
303
|
|
|
* |
|
304
|
|
|
* @return \Spatie\Period\Period|static|null |
|
305
|
|
|
*/ |
|
306
|
|
|
public function overlapSingle(Period $period): ?Period |
|
307
|
|
|
{ |
|
308
|
|
|
$start = $this->getIncludedStart() > $period->getIncludedStart() |
|
309
|
|
|
? $this->getIncludedStart() |
|
310
|
|
|
: $period->getIncludedStart(); |
|
311
|
|
|
|
|
312
|
|
|
$end = $this->getIncludedEnd() < $period->getIncludedEnd() |
|
313
|
|
|
? $this->getIncludedEnd() |
|
314
|
|
|
: $period->getIncludedEnd(); |
|
315
|
|
|
|
|
316
|
|
|
if ($start > $end) { |
|
317
|
|
|
return null; |
|
318
|
|
|
} |
|
319
|
|
|
|
|
320
|
|
|
return static::make($start, $end); |
|
321
|
|
|
} |
|
322
|
|
|
|
|
323
|
|
|
/** |
|
324
|
|
|
* @param \Spatie\Period\Period ...$periods |
|
325
|
|
|
* |
|
326
|
|
|
* @return \Spatie\Period\PeriodCollection|static[] |
|
327
|
|
|
*/ |
|
328
|
|
|
public function overlap(Period ...$periods): PeriodCollection |
|
329
|
|
|
{ |
|
330
|
|
|
$overlapCollection = new PeriodCollection(); |
|
331
|
|
|
|
|
332
|
|
|
foreach ($periods as $period) { |
|
333
|
|
|
$overlapCollection[] = $this->overlapSingle($period); |
|
334
|
|
|
} |
|
335
|
|
|
|
|
336
|
|
|
return $overlapCollection; |
|
337
|
|
|
} |
|
338
|
|
|
|
|
339
|
|
|
/** |
|
340
|
|
|
* @param \Spatie\Period\Period ...$periods |
|
341
|
|
|
* |
|
342
|
|
|
* @return \Spatie\Period\Period|static |
|
343
|
|
|
*/ |
|
344
|
|
|
public function overlapAll(Period ...$periods): Period |
|
345
|
|
|
{ |
|
346
|
|
|
$overlap = clone $this; |
|
347
|
|
|
|
|
348
|
|
|
if (! count($periods)) { |
|
349
|
|
|
return $overlap; |
|
350
|
|
|
} |
|
351
|
|
|
|
|
352
|
|
|
foreach ($periods as $period) { |
|
353
|
|
|
$overlap = $overlap->overlapSingle($period); |
|
354
|
|
|
} |
|
355
|
|
|
|
|
356
|
|
|
return $overlap; |
|
357
|
|
|
} |
|
358
|
|
|
|
|
359
|
|
|
public function diffSingle(Period $period): PeriodCollection |
|
360
|
|
|
{ |
|
361
|
|
|
$periodCollection = new PeriodCollection(); |
|
362
|
|
|
|
|
363
|
|
|
if (! $this->overlapsWith($period)) { |
|
364
|
|
|
$periodCollection[] = clone $this; |
|
365
|
|
|
$periodCollection[] = clone $period; |
|
366
|
|
|
|
|
367
|
|
|
return $periodCollection; |
|
368
|
|
|
} |
|
369
|
|
|
|
|
370
|
|
|
$overlap = $this->overlapSingle($period); |
|
371
|
|
|
|
|
372
|
|
|
$start = $this->getIncludedStart() < $period->getIncludedStart() |
|
373
|
|
|
? $this->getIncludedStart() |
|
374
|
|
|
: $period->getIncludedStart(); |
|
375
|
|
|
|
|
376
|
|
|
$end = $this->getIncludedEnd() > $period->getIncludedEnd() |
|
377
|
|
|
? $this->getIncludedEnd() |
|
378
|
|
|
: $period->getIncludedEnd(); |
|
379
|
|
|
|
|
380
|
|
|
if ($overlap->getIncludedStart() > $start) { |
|
381
|
|
|
$periodCollection[] = static::make( |
|
382
|
|
|
$start, |
|
383
|
|
|
$overlap->getIncludedStart()->sub($this->interval) |
|
384
|
|
|
); |
|
385
|
|
|
} |
|
386
|
|
|
|
|
387
|
|
|
if ($overlap->getIncludedEnd() < $end) { |
|
388
|
|
|
$periodCollection[] = static::make( |
|
389
|
|
|
$overlap->getIncludedEnd()->add($this->interval), |
|
390
|
|
|
$end |
|
391
|
|
|
); |
|
392
|
|
|
} |
|
393
|
|
|
|
|
394
|
|
|
return $periodCollection; |
|
395
|
|
|
} |
|
396
|
|
|
|
|
397
|
|
|
/** |
|
398
|
|
|
* @param \Spatie\Period\Period ...$periods |
|
399
|
|
|
* |
|
400
|
|
|
* @return \Spatie\Period\PeriodCollection|static[] |
|
401
|
|
|
*/ |
|
402
|
|
|
public function diff(Period ...$periods): PeriodCollection |
|
403
|
|
|
{ |
|
404
|
|
|
if (count($periods) === 1 && ! $this->overlapsWith($periods[0])) { |
|
405
|
|
|
$collection = new PeriodCollection(); |
|
406
|
|
|
|
|
407
|
|
|
$collection[] = $this->gap($periods[0]); |
|
408
|
|
|
|
|
409
|
|
|
return $collection; |
|
410
|
|
|
} |
|
411
|
|
|
|
|
412
|
|
|
$diffs = []; |
|
413
|
|
|
|
|
414
|
|
|
foreach ($periods as $period) { |
|
415
|
|
|
$diffs[] = $this->diffSingle($period); |
|
416
|
|
|
} |
|
417
|
|
|
|
|
418
|
|
|
$collection = (new PeriodCollection($this))->overlap(...$diffs); |
|
419
|
|
|
|
|
420
|
|
|
return $collection; |
|
421
|
|
|
} |
|
422
|
|
|
} |
|
423
|
|
|
|
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
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. 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.