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
Push — master ( 8a1645...aea5ce )
by Kyle
17s queued 10s
created

OpeningHours::currentOpenRangeStart()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21

Duplication

Lines 21
Ratio 100 %

Importance

Changes 0
Metric Value
dl 21
loc 21
rs 9.2728
c 0
b 0
f 0
cc 5
nc 5
nop 1
1
<?php
2
3
namespace Spatie\OpeningHours;
4
5
use DateTime;
6
use DateTimeZone;
7
use DateTimeImmutable;
8
use DateTimeInterface;
9
use Spatie\OpeningHours\Helpers\Arr;
10
use Spatie\OpeningHours\Helpers\DataTrait;
11
use Spatie\OpeningHours\Exceptions\Exception;
12
use Spatie\OpeningHours\Exceptions\InvalidDate;
13
use Spatie\OpeningHours\Exceptions\InvalidDayName;
14
use Spatie\OpeningHours\Exceptions\MaximumLimitExceeded;
15
16
class OpeningHours
17
{
18
    const DEFAULT_DAY_LIMIT = 8;
19
20
    use DataTrait;
21
22
    /** @var \Spatie\OpeningHours\Day[] */
23
    protected $openingHours = [];
24
25
    /** @var \Spatie\OpeningHours\OpeningHoursForDay[] */
26
    protected $exceptions = [];
27
28
    /** @var callable[] */
29
    protected $filters = [];
30
31
    /** @var DateTimeZone|null */
32
    protected $timezone = null;
33
34
    /** @var bool Allow for overflowing time ranges which overflow into the next day */
35
    protected $overflow;
36
37
    /** @var int Number of days to try before abandoning the search of the next close/open time */
38
    protected $dayLimit = null;
39
40
    public function __construct($timezone = null)
41
    {
42
        if ($timezone instanceof DateTimeZone) {
43
            $this->timezone = $timezone;
44
        } elseif (is_string($timezone)) {
45
            $this->timezone = new DateTimeZone($timezone);
46
        } elseif ($timezone) {
47
            throw new \InvalidArgumentException('Invalid Timezone');
48
        }
49
50
        $this->openingHours = Day::mapDays(function () {
51
            return new OpeningHoursForDay();
52
        });
53
    }
54
55
    /**
56
     * @param string[][]               $data
57
     * @param string|DateTimeZone|null $timezone
58
     *
59
     * @return static
60
     */
61
    public static function create(array $data, $timezone = null): self
62
    {
63
        return (new static($timezone))->fill($data);
64
    }
65
66
    /**
67
     * @param array $data
68
     *
69
     * @return array
70
     */
71
    public static function mergeOverlappingRanges(array $data)
72
    {
73
        $result = [];
74
        $ranges = [];
75
        foreach ($data as $key => $value) {
76
            $value = is_array($value)
77
                ? static::mergeOverlappingRanges($value)
78
                : (is_string($value) ? TimeRange::fromString($value) : $value);
79
80
            if ($value instanceof TimeRange) {
81
                $newRanges = [];
82
                foreach ($ranges as $range) {
83
                    if ($value->format() === $range->format()) {
84
                        continue 2;
85
                    }
86
87
                    if ($value->overlaps($range) || $range->overlaps($value)) {
88
                        $value = TimeRange::fromList([$value, $range]);
89
90
                        continue;
91
                    }
92
93
                    $newRanges[] = $range;
94
                }
95
96
                $newRanges[] = $value;
97
                $ranges = $newRanges;
98
99
                continue;
100
            }
101
102
            $result[$key] = $value;
103
        }
104
105
        foreach ($ranges as $range) {
106
            $result[] = $range;
107
        }
108
109
        return $result;
110
    }
111
112
    /**
113
     * @param string[][]               $data
114
     * @param string|DateTimeZone|null $timezone
115
     *
116
     * @return static
117
     */
118
    public static function createAndMergeOverlappingRanges(array $data, $timezone = null)
119
    {
120
        return static::create(static::mergeOverlappingRanges($data), $timezone);
121
    }
122
123
    /**
124
     * @param array $data
125
     *
126
     * @return bool
127
     */
128
    public static function isValid(array $data): bool
129
    {
130
        try {
131
            static::create($data);
132
133
            return true;
134
        } catch (Exception $exception) {
135
            return false;
136
        }
137
    }
138
139
    /**
140
     * Set the number of days to try before abandoning the search of the next close/open time.
141
     *
142
     * @param int $dayLimit number of days
143
     */
144
    public function setDayLimit(int $dayLimit)
145
    {
146
        $this->dayLimit = $dayLimit;
147
    }
148
149
    /**
150
     * Get the number of days to try before abandoning the search of the next close/open time.
151
     *
152
     * @return int
153
     */
154
    public function getDayLimit(): int
155
    {
156
        return $this->dayLimit ?: static::DEFAULT_DAY_LIMIT;
157
    }
158
159
    public function setFilters(array $filters)
160
    {
161
        $this->filters = $filters;
162
163
        return $this;
164
    }
165
166
    public function getFilters(): array
167
    {
168
        return $this->filters;
169
    }
170
171
    public function fill(array $data)
172
    {
173
        list($openingHours, $exceptions, $metaData, $filters, $overflow) = $this->parseOpeningHoursAndExceptions($data);
174
175
        $this->overflow = $overflow;
176
177
        foreach ($openingHours as $day => $openingHoursForThisDay) {
178
            $this->setOpeningHoursFromStrings($day, $openingHoursForThisDay);
179
        }
180
181
        $this->setExceptionsFromStrings($exceptions);
182
183
        return $this->setFilters($filters)->setData($metaData);
184
    }
185
186
    public function forWeek(): array
187
    {
188
        return $this->openingHours;
189
    }
190
191
    public function forWeekCombined(): array
192
    {
193
        $equalDays = [];
194
        $allOpeningHours = $this->openingHours;
195
        $uniqueOpeningHours = array_unique($allOpeningHours);
196
        $nonUniqueOpeningHours = $allOpeningHours;
197
198
        foreach ($uniqueOpeningHours as $day => $value) {
199
            $equalDays[$day] = ['days' => [$day], 'opening_hours' => $value];
200
            unset($nonUniqueOpeningHours[$day]);
201
        }
202
203
        foreach ($uniqueOpeningHours as $uniqueDay => $uniqueValue) {
204
            foreach ($nonUniqueOpeningHours as $nonUniqueDay => $nonUniqueValue) {
205
                if ((string) $uniqueValue === (string) $nonUniqueValue) {
206
                    $equalDays[$uniqueDay]['days'][] = $nonUniqueDay;
207
                }
208
            }
209
        }
210
211
        return $equalDays;
212
    }
213
214
    public function forDay(string $day): OpeningHoursForDay
215
    {
216
        $day = $this->normalizeDayName($day);
217
218
        return $this->openingHours[$day];
219
    }
220
221
    public function forDate(DateTimeInterface $date): OpeningHoursForDay
222
    {
223
        $date = $this->applyTimezone($date);
224
225
        foreach ($this->filters as $filter) {
226
            $result = $filter($date);
227
228
            if (is_array($result)) {
229
                return OpeningHoursForDay::fromStrings($result);
230
            }
231
        }
232
233
        return $this->exceptions[$date->format('Y-m-d')] ?? ($this->exceptions[$date->format('m-d')] ?? $this->forDay(Day::onDateTime($date)));
234
    }
235
236
    /**
237
     * @param DateTimeInterface $date
238
     *
239
     * @return TimeRange[]
240
     */
241
    public function forDateTime(DateTimeInterface $date): array
242
    {
243
        $yesterday = $date;
244
245
        if (! ($yesterday instanceof DateTimeImmutable)) {
246
            $yesterday = clone $yesterday;
247
        }
248
249
        $yesterday = $yesterday->modify('-1 day');
250
251
        return array_merge(
252
            iterator_to_array($this->forDate($yesterday)->forNightTime(Time::fromDateTime($date))),
253
            iterator_to_array($this->forDate($date)->forTime(Time::fromDateTime($date)))
254
        );
255
    }
256
257
    public function exceptions(): array
258
    {
259
        return $this->exceptions;
260
    }
261
262
    public function isOpenOn(string $day): bool
263
    {
264
        return count($this->forDay($day)) > 0;
265
    }
266
267
    public function isClosedOn(string $day): bool
268
    {
269
        return ! $this->isOpenOn($day);
270
    }
271
272
    public function isOpenAt(DateTimeInterface $dateTime): bool
273
    {
274
        $dateTime = $this->applyTimezone($dateTime);
275
276
        if ($this->overflow) {
277
            $yesterdayDateTime = $dateTime;
278
            if (! ($yesterdayDateTime instanceof DateTimeImmutable)) {
279
                $yesterdayDateTime = clone $yesterdayDateTime;
280
            }
281
            $dateTimeMinus1Day = $yesterdayDateTime->sub(new \DateInterval('P1D'));
282
            $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day);
283
            if ($openingHoursForDayBefore->isOpenAtNight(Time::fromDateTime($dateTimeMinus1Day))) {
284
                return true;
285
            }
286
        }
287
288
        $openingHoursForDay = $this->forDate($dateTime);
289
290
        return $openingHoursForDay->isOpenAt(Time::fromDateTime($dateTime));
291
    }
292
293
    public function isClosedAt(DateTimeInterface $dateTime): bool
294
    {
295
        return ! $this->isOpenAt($dateTime);
296
    }
297
298
    public function isOpen(): bool
299
    {
300
        return $this->isOpenAt(new DateTime());
301
    }
302
303
    public function isClosed(): bool
304
    {
305
        return $this->isClosedAt(new DateTime());
306
    }
307
308
    public function nextOpen(DateTimeInterface $dateTime): DateTimeInterface
309
    {
310
        if (! ($dateTime instanceof DateTimeImmutable)) {
311
            $dateTime = clone $dateTime;
312
        }
313
314
        $openingHoursForDay = $this->forDate($dateTime);
315
        $nextOpen = $openingHoursForDay->nextOpen(Time::fromDateTime($dateTime));
316
        $tries = $this->getDayLimit();
317
318 View Code Duplication
        while ($nextOpen === false || $nextOpen->hours() >= 24) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
319
            if (--$tries < 0) {
320
                throw MaximumLimitExceeded::forString(
321
                    'No open date/time found in the next '.$this->getDayLimit().' days,'.
322
                    ' use $openingHours->setDayLimit() to increase the limit.'
323
                );
324
            }
325
326
            $dateTime = $dateTime
327
                ->modify('+1 day')
328
                ->setTime(0, 0, 0);
329
330
            if ($this->isOpenAt($dateTime) && ! $openingHoursForDay->isOpenAt(Time::fromString('23:59'))) {
331
                return $dateTime;
332
            }
333
334
            $openingHoursForDay = $this->forDate($dateTime);
335
336
            $nextOpen = $openingHoursForDay->nextOpen(Time::fromDateTime($dateTime));
337
        }
338
339
        if ($dateTime->format('H:i') === '00:00' && $this->isOpenAt((clone $dateTime)->modify('-1 second'))) {
340
            return $this->nextOpen($dateTime->modify('+1 minute'));
341
        }
342
343
        $nextDateTime = $nextOpen->toDateTime();
344
345
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
346
    }
347
348
    public function currentOpenRange(DateTimeInterface $dateTime)
349
    {
350
        $list = $this->forDateTime($dateTime);
351
352
        return end($list) ?: false;
353
    }
354
355 View Code Duplication
    public function currentOpenRangeStart(DateTimeInterface $dateTime)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
356
    {
357
        /** @var TimeRange $range */
358
        $range = $this->currentOpenRange($dateTime);
359
360
        if (! $range) {
361
            return false;
362
        }
363
364
        if (! ($dateTime instanceof DateTimeImmutable)) {
365
            $dateTime = clone $dateTime;
366
        }
367
368
        $nextDateTime = $range->start()->toDateTime();
369
370
        if ($range->overflowsNextDay() && $nextDateTime->format('Hi') > $dateTime->format('Hi')) {
371
            $dateTime = $dateTime->modify('-1 day');
372
        }
373
374
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
375
    }
376
377 View Code Duplication
    public function currentOpenRangeEnd(DateTimeInterface $dateTime)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
378
    {
379
        /** @var TimeRange $range */
380
        $range = $this->currentOpenRange($dateTime);
381
382
        if (! $range) {
383
            return false;
384
        }
385
386
        if (! ($dateTime instanceof DateTimeImmutable)) {
387
            $dateTime = clone $dateTime;
388
        }
389
390
        $nextDateTime = $range->end()->toDateTime();
391
392
        if ($range->overflowsNextDay() && $nextDateTime->format('Hi') < $dateTime->format('Hi')) {
393
            $dateTime = $dateTime->modify('+1 day');
394
        }
395
396
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
397
    }
398
399
    public function nextClose(DateTimeInterface $dateTime): DateTimeInterface
400
    {
401
        if (! ($dateTime instanceof DateTimeImmutable)) {
402
            $dateTime = clone $dateTime;
403
        }
404
405
        $nextClose = null;
406
        if ($this->overflow) {
407
            $yesterday = $dateTime;
408
            if (! ($dateTime instanceof DateTimeImmutable)) {
409
                $yesterday = clone $dateTime;
410
            }
411
            $dateTimeMinus1Day = $yesterday->sub(new \DateInterval('P1D'));
412
            $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day);
413
            if ($openingHoursForDayBefore->isOpenAtNight(Time::fromDateTime($dateTimeMinus1Day))) {
414
                $nextClose = $openingHoursForDayBefore->nextClose(Time::fromDateTime($dateTime));
415
            }
416
        }
417
418
        $openingHoursForDay = $this->forDate($dateTime);
419
        if (! $nextClose) {
420
            $nextClose = $openingHoursForDay->nextClose(Time::fromDateTime($dateTime));
421
        }
422
423
        $tries = $this->getDayLimit();
424
425 View Code Duplication
        while ($nextClose === false || $nextClose->hours() >= 24) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
426
            if (--$tries < 0) {
427
                throw MaximumLimitExceeded::forString(
428
                    'No close date/time found in the next '.$this->getDayLimit().' days,'.
429
                    ' use $openingHours->setDayLimit() to increase the limit.'
430
                );
431
            }
432
433
            $dateTime = $dateTime
434
                ->modify('+1 day')
435
                ->setTime(0, 0, 0);
436
437
            if ($this->isClosedAt($dateTime) && $openingHoursForDay->isOpenAt(Time::fromString('23:59'))) {
438
                return $dateTime;
439
            }
440
441
            $openingHoursForDay = $this->forDate($dateTime);
442
443
            $nextClose = $openingHoursForDay->nextClose(Time::fromDateTime($dateTime));
444
        }
445
446
        $nextDateTime = $nextClose->toDateTime();
447
448
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
449
    }
450
451
    public function regularClosingDays(): array
452
    {
453
        return array_keys($this->filter(function (OpeningHoursForDay $openingHoursForDay) {
454
            return $openingHoursForDay->isEmpty();
455
        }));
456
    }
457
458
    public function regularClosingDaysISO(): array
459
    {
460
        return Arr::map($this->regularClosingDays(), [Day::class, 'toISO']);
461
    }
462
463
    public function exceptionalClosingDates(): array
464
    {
465
        $dates = array_keys($this->filterExceptions(function (OpeningHoursForDay $openingHoursForDay) {
466
            return $openingHoursForDay->isEmpty();
467
        }));
468
469
        return Arr::map($dates, function ($date) {
470
            return DateTime::createFromFormat('Y-m-d', $date);
471
        });
472
    }
473
474
    public function setTimezone($timezone)
475
    {
476
        $this->timezone = new DateTimeZone($timezone);
477
    }
478
479
    protected function parseOpeningHoursAndExceptions(array $data): array
480
    {
481
        $metaData = Arr::pull($data, 'data', null);
482
        $exceptions = [];
483
        $filters = Arr::pull($data, 'filters', []);
484
        $overflow = (bool) Arr::pull($data, 'overflow', false);
485
486
        foreach (Arr::pull($data, 'exceptions', []) as $key => $exception) {
487
            if (is_callable($exception)) {
488
                $filters[] = $exception;
489
490
                continue;
491
            }
492
493
            $exceptions[$key] = $exception;
494
        }
495
496
        $openingHours = [];
497
498
        foreach ($data as $day => $openingHoursData) {
499
            $openingHours[$this->normalizeDayName($day)] = $openingHoursData;
500
        }
501
502
        return [$openingHours, $exceptions, $metaData, $filters, $overflow];
503
    }
504
505
    protected function setOpeningHoursFromStrings(string $day, array $openingHours)
506
    {
507
        $day = $this->normalizeDayName($day);
508
509
        $data = null;
510
511
        if (isset($openingHours['data'])) {
512
            $data = $openingHours['data'];
513
            unset($openingHours['data']);
514
        }
515
516
        $this->openingHours[$day] = OpeningHoursForDay::fromStrings($openingHours)->setData($data);
517
    }
518
519
    protected function setExceptionsFromStrings(array $exceptions)
520
    {
521
        if (empty($exceptions)) {
522
            return;
523
        }
524
525
        if (! $this->dayLimit) {
526
            $this->dayLimit = 366;
527
        }
528
529
        $this->exceptions = Arr::map($exceptions, function (array $openingHours, string $date) {
530
            $recurring = DateTime::createFromFormat('m-d', $date);
531
532
            if ($recurring === false || $recurring->format('m-d') !== $date) {
533
                $dateTime = DateTime::createFromFormat('Y-m-d', $date);
534
535
                if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
536
                    throw InvalidDate::invalidDate($date);
537
                }
538
            }
539
540
            return OpeningHoursForDay::fromStrings($openingHours);
541
        });
542
    }
543
544
    protected function normalizeDayName(string $day)
545
    {
546
        $day = strtolower($day);
547
548
        if (! Day::isValid($day)) {
549
            throw InvalidDayName::invalidDayName($day);
550
        }
551
552
        return $day;
553
    }
554
555
    protected function applyTimezone(DateTimeInterface $date)
556
    {
557
        if ($this->timezone) {
558
            $date = $date->setTimezone($this->timezone);
559
        }
560
561
        return $date;
562
    }
563
564
    public function filter(callable $callback): array
565
    {
566
        return Arr::filter($this->openingHours, $callback);
567
    }
568
569
    public function map(callable $callback): array
570
    {
571
        return Arr::map($this->openingHours, $callback);
572
    }
573
574
    public function flatMap(callable $callback): array
575
    {
576
        return Arr::flatMap($this->openingHours, $callback);
577
    }
578
579
    public function filterExceptions(callable $callback): array
580
    {
581
        return Arr::filter($this->exceptions, $callback);
582
    }
583
584
    public function mapExceptions(callable $callback): array
585
    {
586
        return Arr::map($this->exceptions, $callback);
587
    }
588
589
    public function flatMapExceptions(callable $callback): array
590
    {
591
        return Arr::flatMap($this->exceptions, $callback);
592
    }
593
594
    public function asStructuredData(string $format = 'H:i', $timezone = null): array
595
    {
596 View Code Duplication
        $regularHours = $this->flatMap(function (OpeningHoursForDay $openingHoursForDay, string $day) use ($format, $timezone) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
597
            return $openingHoursForDay->map(function (TimeRange $timeRange) use ($format, $timezone, $day) {
598
                return [
599
                    '@type' => 'OpeningHoursSpecification',
600
                    'dayOfWeek' => ucfirst($day),
601
                    'opens' => $timeRange->start()->format($format, $timezone),
602
                    'closes' => $timeRange->end()->format($format, $timezone),
603
                ];
604
            });
605
        });
606
607
        $exceptions = $this->flatMapExceptions(function (OpeningHoursForDay $openingHoursForDay, string $date) use ($format, $timezone) {
608
            if ($openingHoursForDay->isEmpty()) {
609
                $zero = Time::fromString('00:00')->format($format, $timezone);
610
611
                return [[
612
                    '@type' => 'OpeningHoursSpecification',
613
                    'opens' => $zero,
614
                    'closes' => $zero,
615
                    'validFrom' => $date,
616
                    'validThrough' => $date,
617
                ]];
618
            }
619
620 View Code Duplication
            return $openingHoursForDay->map(function (TimeRange $timeRange) use ($format, $date, $timezone) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
621
                return [
622
                    '@type' => 'OpeningHoursSpecification',
623
                    'opens' => $timeRange->start()->format($format, $timezone),
624
                    'closes' => $timeRange->end()->format($format, $timezone),
625
                    'validFrom' => $date,
626
                    'validThrough' => $date,
627
                ];
628
            });
629
        });
630
631
        return array_merge($regularHours, $exceptions);
632
    }
633
}
634