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.

OpeningHours   F
last analyzed

Complexity

Total Complexity 128

Size/Duplication

Total Lines 708
Duplicated Lines 21.19 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 128
lcom 1
cbo 8
dl 150
loc 708
rs 1.892
c 0
b 0
f 0

46 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 4
A create() 0 4 1
B mergeOverlappingRanges() 0 44 11
A createAndMergeOverlappingRanges() 0 4 1
A isValid() 0 10 2
A setDayLimit() 0 4 1
A getDayLimit() 0 4 2
A setFilters() 0 6 1
A getFilters() 0 4 1
A fill() 0 14 2
A forWeek() 0 4 1
A forWeekCombined() 0 22 5
A forWeekConsecutiveDays() 0 20 4
A forDay() 0 6 1
A forDate() 0 14 3
A forDateTime() 0 9 1
A exceptions() 0 4 1
A isOpenOn() 0 11 3
A isClosedOn() 0 4 1
A isOpenAt() 0 16 3
A isClosedAt() 0 4 1
A isOpen() 0 4 1
A isClosed() 0 4 1
A currentOpenRange() 0 6 2
A currentOpenRangeStart() 19 19 4
A currentOpenRangeEnd() 19 19 4
B nextOpen() 20 36 8
C nextClose() 27 48 12
B previousOpen() 20 32 7
B previousClose() 26 43 10
A regularClosingDays() 0 6 1
A regularClosingDaysISO() 0 4 1
A exceptionalClosingDates() 0 10 1
A setTimezone() 0 4 1
A parseOpeningHoursAndExceptions() 0 25 4
A setOpeningHoursFromStrings() 0 13 2
B setExceptionsFromStrings() 0 24 7
A normalizeDayName() 0 10 2
A applyTimezone() 0 8 2
A filter() 0 4 1
A map() 0 4 1
A flatMap() 0 4 1
A filterExceptions() 0 4 1
A mapExceptions() 0 4 1
A flatMapExceptions() 0 4 1
A asStructuredData() 19 39 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OpeningHours often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OpeningHours, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Spatie\OpeningHours;
4
5
use DateTime;
6
use DateTimeInterface;
7
use DateTimeZone;
8
use Spatie\OpeningHours\Exceptions\Exception;
9
use Spatie\OpeningHours\Exceptions\InvalidDate;
10
use Spatie\OpeningHours\Exceptions\InvalidDayName;
11
use Spatie\OpeningHours\Exceptions\InvalidTimezone;
12
use Spatie\OpeningHours\Exceptions\MaximumLimitExceeded;
13
use Spatie\OpeningHours\Helpers\Arr;
14
use Spatie\OpeningHours\Helpers\DataTrait;
15
use Spatie\OpeningHours\Helpers\DateTimeCopier;
16
use Spatie\OpeningHours\Helpers\DiffTrait;
17
18
class OpeningHours
19
{
20
    const DEFAULT_DAY_LIMIT = 8;
21
22
    use DataTrait, DateTimeCopier, DiffTrait;
23
24
    /** @var \Spatie\OpeningHours\Day[] */
25
    protected $openingHours = [];
26
27
    /** @var \Spatie\OpeningHours\OpeningHoursForDay[] */
28
    protected $exceptions = [];
29
30
    /** @var callable[] */
31
    protected $filters = [];
32
33
    /** @var DateTimeZone|null */
34
    protected $timezone = null;
35
36
    /** @var bool Allow for overflowing time ranges which overflow into the next day */
37
    protected $overflow;
38
39
    /** @var int Number of days to try before abandoning the search of the next close/open time */
40
    protected $dayLimit = null;
41
42
    public function __construct($timezone = null)
43
    {
44
        if ($timezone instanceof DateTimeZone) {
45
            $this->timezone = $timezone;
46
        } elseif (is_string($timezone)) {
47
            $this->timezone = new DateTimeZone($timezone);
48
        } elseif ($timezone) {
49
            throw InvalidTimezone::create();
50
        }
51
52
        $this->openingHours = Day::mapDays(function () {
53
            return new OpeningHoursForDay();
54
        });
55
    }
56
57
    /**
58
     * @param string[][]               $data
59
     * @param string|DateTimeZone|null $timezone
60
     *
61
     * @return static
62
     */
63
    public static function create(array $data, $timezone = null): self
64
    {
65
        return (new static($timezone))->fill($data);
66
    }
67
68
    /**
69
     * @param array $data         hours definition array or sub-array
70
     * @param array $excludedKeys keys to ignore from parsing
71
     *
72
     * @return array
73
     */
74
    public static function mergeOverlappingRanges(array $data, array $excludedKeys = ['data', 'filters', 'overflow'])
75
    {
76
        $result = [];
77
        $ranges = [];
78
        foreach ($data as $key => $value) {
79
            if (in_array($key, $excludedKeys, true)) {
80
                continue;
81
            }
82
83
            $value = is_array($value)
84
                ? static::mergeOverlappingRanges($value, ['data'])
85
                : (is_string($value) ? TimeRange::fromString($value) : $value);
86
87
            if ($value instanceof TimeRange) {
88
                $newRanges = [];
89
                foreach ($ranges as $range) {
90
                    if ($value->format() === $range->format()) {
91
                        continue 2;
92
                    }
93
94
                    if ($value->overlaps($range) || $range->overlaps($value)) {
95
                        $value = TimeRange::fromList([$value, $range]);
96
97
                        continue;
98
                    }
99
100
                    $newRanges[] = $range;
101
                }
102
103
                $newRanges[] = $value;
104
                $ranges = $newRanges;
105
106
                continue;
107
            }
108
109
            $result[$key] = $value;
110
        }
111
112
        foreach ($ranges as $range) {
113
            $result[] = $range;
114
        }
115
116
        return $result;
117
    }
118
119
    /**
120
     * @param string[][]               $data
121
     * @param string|DateTimeZone|null $timezone
122
     *
123
     * @return static
124
     */
125
    public static function createAndMergeOverlappingRanges(array $data, $timezone = null)
126
    {
127
        return static::create(static::mergeOverlappingRanges($data), $timezone);
128
    }
129
130
    /**
131
     * @param array $data
132
     *
133
     * @return bool
134
     */
135
    public static function isValid(array $data): bool
136
    {
137
        try {
138
            static::create($data);
139
140
            return true;
141
        } catch (Exception $exception) {
142
            return false;
143
        }
144
    }
145
146
    /**
147
     * Set the number of days to try before abandoning the search of the next close/open time.
148
     *
149
     * @param int $dayLimit number of days
150
     */
151
    public function setDayLimit(int $dayLimit)
152
    {
153
        $this->dayLimit = $dayLimit;
154
    }
155
156
    /**
157
     * Get the number of days to try before abandoning the search of the next close/open time.
158
     *
159
     * @return int
160
     */
161
    public function getDayLimit(): int
162
    {
163
        return $this->dayLimit ?: static::DEFAULT_DAY_LIMIT;
164
    }
165
166
    public function setFilters(array $filters)
167
    {
168
        $this->filters = $filters;
169
170
        return $this;
171
    }
172
173
    public function getFilters(): array
174
    {
175
        return $this->filters;
176
    }
177
178
    public function fill(array $data)
179
    {
180
        list($openingHours, $exceptions, $metaData, $filters, $overflow) = $this->parseOpeningHoursAndExceptions($data);
181
182
        $this->overflow = $overflow;
183
184
        foreach ($openingHours as $day => $openingHoursForThisDay) {
185
            $this->setOpeningHoursFromStrings($day, $openingHoursForThisDay);
186
        }
187
188
        $this->setExceptionsFromStrings($exceptions);
189
190
        return $this->setFilters($filters)->setData($metaData);
191
    }
192
193
    public function forWeek(): array
194
    {
195
        return $this->openingHours;
196
    }
197
198
    public function forWeekCombined(): array
199
    {
200
        $equalDays = [];
201
        $allOpeningHours = $this->openingHours;
202
        $uniqueOpeningHours = array_unique($allOpeningHours);
203
        $nonUniqueOpeningHours = $allOpeningHours;
204
205
        foreach ($uniqueOpeningHours as $day => $value) {
206
            $equalDays[$day] = ['days' => [$day], 'opening_hours' => $value];
207
            unset($nonUniqueOpeningHours[$day]);
208
        }
209
210
        foreach ($uniqueOpeningHours as $uniqueDay => $uniqueValue) {
211
            foreach ($nonUniqueOpeningHours as $nonUniqueDay => $nonUniqueValue) {
212
                if ((string) $uniqueValue === (string) $nonUniqueValue) {
213
                    $equalDays[$uniqueDay]['days'][] = $nonUniqueDay;
214
                }
215
            }
216
        }
217
218
        return $equalDays;
219
    }
220
221
    public function forWeekConsecutiveDays(): array
222
    {
223
        $concatenatedDays = [];
224
        $allOpeningHours = $this->openingHours;
225
        foreach ($allOpeningHours as $day => $value) {
226
            $previousDay = end($concatenatedDays);
227
            if ($previousDay && (string) $previousDay['opening_hours'] === (string) $value) {
228
                $key = key($concatenatedDays);
229
                $concatenatedDays[$key]['days'][] = $day;
230
                continue;
231
            }
232
233
            $concatenatedDays[$day] = [
234
                'opening_hours' => $value,
235
                'days' => [$day],
236
            ];
237
        }
238
239
        return $concatenatedDays;
240
    }
241
242
    public function forDay(string $day): OpeningHoursForDay
243
    {
244
        $day = $this->normalizeDayName($day);
245
246
        return $this->openingHours[$day];
247
    }
248
249
    public function forDate(DateTimeInterface $date): OpeningHoursForDay
250
    {
251
        $date = $this->applyTimezone($date);
252
253
        foreach ($this->filters as $filter) {
254
            $result = $filter($date);
255
256
            if (is_array($result)) {
257
                return OpeningHoursForDay::fromStrings($result);
258
            }
259
        }
260
261
        return $this->exceptions[$date->format('Y-m-d')] ?? ($this->exceptions[$date->format('m-d')] ?? $this->forDay(Day::onDateTime($date)));
262
    }
263
264
    /**
265
     * @param DateTimeInterface $date
266
     *
267
     * @return TimeRange[]
268
     */
269
    public function forDateTime(DateTimeInterface $date): array
270
    {
271
        return array_merge(
272
            iterator_to_array($this->forDate(
273
                $this->yesterday($date)
274
            )->forNightTime(Time::fromDateTime($date))),
275
            iterator_to_array($this->forDate($date)->forTime(Time::fromDateTime($date)))
276
        );
277
    }
278
279
    public function exceptions(): array
280
    {
281
        return $this->exceptions;
282
    }
283
284
    public function isOpenOn(string $day): bool
285
    {
286
        if (preg_match('/^(?:(\d+)-)?(\d{1,2})-(\d{1,2})$/', $day, $match)) {
287
            list(, $year, $month, $day) = $match;
288
            $year = $year ?: date('Y');
289
290
            return count($this->forDate(new DateTime("$year-$month-$day"))) > 0;
291
        }
292
293
        return count($this->forDay($day)) > 0;
294
    }
295
296
    public function isClosedOn(string $day): bool
297
    {
298
        return ! $this->isOpenOn($day);
299
    }
300
301
    public function isOpenAt(DateTimeInterface $dateTime): bool
302
    {
303
        $dateTime = $this->applyTimezone($dateTime);
304
305
        if ($this->overflow) {
306
            $dateTimeMinus1Day = $this->yesterday($dateTime);
307
            $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day);
308
            if ($openingHoursForDayBefore->isOpenAtNight(Time::fromDateTime($dateTimeMinus1Day))) {
309
                return true;
310
            }
311
        }
312
313
        $openingHoursForDay = $this->forDate($dateTime);
314
315
        return $openingHoursForDay->isOpenAt(Time::fromDateTime($dateTime));
316
    }
317
318
    public function isClosedAt(DateTimeInterface $dateTime): bool
319
    {
320
        return ! $this->isOpenAt($dateTime);
321
    }
322
323
    public function isOpen(): bool
324
    {
325
        return $this->isOpenAt(new DateTime());
326
    }
327
328
    public function isClosed(): bool
329
    {
330
        return $this->isClosedAt(new DateTime());
331
    }
332
333
    public function currentOpenRange(DateTimeInterface $dateTime)
334
    {
335
        $list = $this->forDateTime($dateTime);
336
337
        return end($list) ?: false;
338
    }
339
340 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...
341
    {
342
        /** @var TimeRange $range */
343
        $range = $this->currentOpenRange($dateTime);
344
345
        if (! $range) {
346
            return false;
347
        }
348
349
        $dateTime = $this->copyDateTime($dateTime);
350
351
        $nextDateTime = $range->start()->toDateTime();
352
353
        if ($range->overflowsNextDay() && $nextDateTime->format('Hi') > $dateTime->format('Hi')) {
354
            $dateTime = $dateTime->modify('-1 day');
355
        }
356
357
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
358
    }
359
360 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...
361
    {
362
        /** @var TimeRange $range */
363
        $range = $this->currentOpenRange($dateTime);
364
365
        if (! $range) {
366
            return false;
367
        }
368
369
        $dateTime = $this->copyDateTime($dateTime);
370
371
        $nextDateTime = $range->end()->toDateTime();
372
373
        if ($range->overflowsNextDay() && $nextDateTime->format('Hi') < $dateTime->format('Hi')) {
374
            $dateTime = $dateTime->modify('+1 day');
375
        }
376
377
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
378
    }
379
380
    public function nextOpen(DateTimeInterface $dateTime): DateTimeInterface
381
    {
382
        $dateTime = $this->copyDateTime($dateTime);
383
        $openingHoursForDay = $this->forDate($dateTime);
384
        $nextOpen = $openingHoursForDay->nextOpen(Time::fromDateTime($dateTime));
385
        $tries = $this->getDayLimit();
386
387 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...
388
            if (--$tries < 0) {
389
                throw MaximumLimitExceeded::forString(
390
                    'No open date/time found in the next '.$this->getDayLimit().' days,'.
391
                    ' use $openingHours->setDayLimit() to increase the limit.'
392
                );
393
            }
394
395
            $dateTime = $dateTime
396
                ->modify('+1 day')
397
                ->setTime(0, 0, 0);
398
399
            if ($this->isOpenAt($dateTime) && ! $openingHoursForDay->isOpenAt(Time::fromString('23:59'))) {
400
                return $dateTime;
401
            }
402
403
            $openingHoursForDay = $this->forDate($dateTime);
404
405
            $nextOpen = $openingHoursForDay->nextOpen(Time::fromDateTime($dateTime));
406
        }
407
408
        if ($dateTime->format('H:i') === '00:00' && $this->isOpenAt((clone $dateTime)->modify('-1 second'))) {
409
            return $this->nextOpen($dateTime->modify('+1 minute'));
410
        }
411
412
        $nextDateTime = $nextOpen->toDateTime();
413
414
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
415
    }
416
417
    public function nextClose(DateTimeInterface $dateTime): DateTimeInterface
418
    {
419
        $dateTime = $this->copyDateTime($dateTime);
420
        $nextClose = null;
421 View Code Duplication
        if ($this->overflow) {
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...
422
            $dateTimeMinus1Day = $this->copyDateTime($dateTime)->modify('-1 day');
423
            $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day);
424
            if ($openingHoursForDayBefore->isOpenAtNight(Time::fromDateTime($dateTimeMinus1Day))) {
425
                $nextClose = $openingHoursForDayBefore->nextClose(Time::fromDateTime($dateTime));
426
            }
427
        }
428
429
        $openingHoursForDay = $this->forDate($dateTime);
430
        if (! $nextClose) {
431
            $nextClose = $openingHoursForDay->nextClose(Time::fromDateTime($dateTime));
432
433
            if ($nextClose && $nextClose->hours() < 24 && $nextClose->format('Gi') < $dateTime->format('Gi')) {
434
                $dateTime = $dateTime->modify('+1 day');
435
            }
436
        }
437
438
        $tries = $this->getDayLimit();
439
440 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...
441
            if (--$tries < 0) {
442
                throw MaximumLimitExceeded::forString(
443
                    'No close date/time found in the next '.$this->getDayLimit().' days,'.
444
                    ' use $openingHours->setDayLimit() to increase the limit.'
445
                );
446
            }
447
448
            $dateTime = $dateTime
449
                ->modify('+1 day')
450
                ->setTime(0, 0, 0);
451
452
            if ($this->isClosedAt($dateTime) && $openingHoursForDay->isOpenAt(Time::fromString('23:59'))) {
453
                return $dateTime;
454
            }
455
456
            $openingHoursForDay = $this->forDate($dateTime);
457
458
            $nextClose = $openingHoursForDay->nextClose(Time::fromDateTime($dateTime));
459
        }
460
461
        $nextDateTime = $nextClose->toDateTime();
462
463
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
464
    }
465
466
    public function previousOpen(DateTimeInterface $dateTime): DateTimeInterface
467
    {
468
        $dateTime = $this->copyDateTime($dateTime);
469
        $openingHoursForDay = $this->forDate($dateTime);
470
        $previousOpen = $openingHoursForDay->previousOpen(Time::fromDateTime($dateTime));
471
        $tries = $this->getDayLimit();
472
473 View Code Duplication
        while ($previousOpen === false || ($previousOpen->hours() === 0 && $previousOpen->minutes() === 0)) {
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...
474
            if (--$tries < 0) {
475
                throw MaximumLimitExceeded::forString(
476
                    'No open date/time found in the previous '.$this->getDayLimit().' days,'.
477
                    ' use $openingHours->setDayLimit() to increase the limit.'
478
                );
479
            }
480
481
            $midnight = $dateTime->setTime(0, 0, 0);
482
            $dateTime = clone $midnight;
483
            $dateTime = $dateTime->modify('-1 minute');
484
485
            $openingHoursForDay = $this->forDate($dateTime);
486
487
            if ($this->isOpenAt($midnight) && ! $openingHoursForDay->isOpenAt(Time::fromString('23:59'))) {
488
                return $midnight;
489
            }
490
491
            $previousOpen = $openingHoursForDay->previousOpen(Time::fromDateTime($dateTime));
492
        }
493
494
        $nextDateTime = $previousOpen->toDateTime();
495
496
        return $dateTime->setTime($nextDateTime->format('G'), $nextDateTime->format('i'), 0);
497
    }
498
499
    public function previousClose(DateTimeInterface $dateTime): DateTimeInterface
500
    {
501
        $dateTime = $this->copyDateTime($dateTime);
502
        $previousClose = null;
503 View Code Duplication
        if ($this->overflow) {
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...
504
            $dateTimeMinus1Day = $this->copyDateTime($dateTime)->modify('-1 day');
505
            $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day);
506
            if ($openingHoursForDayBefore->isOpenAtNight(Time::fromDateTime($dateTimeMinus1Day))) {
507
                $previousClose = $openingHoursForDayBefore->previousClose(Time::fromDateTime($dateTime));
508
            }
509
        }
510
511
        $openingHoursForDay = $this->forDate($dateTime);
512
        if (! $previousClose) {
513
            $previousClose = $openingHoursForDay->previousClose(Time::fromDateTime($dateTime));
514
        }
515
516
        $tries = $this->getDayLimit();
517
518 View Code Duplication
        while ($previousClose === false || ($previousClose->hours() === 0 && $previousClose->minutes() === 0)) {
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...
519
            if (--$tries < 0) {
520
                throw MaximumLimitExceeded::forString(
521
                    'No close date/time found in the previous '.$this->getDayLimit().' days,'.
522
                    ' use $openingHours->setDayLimit() to increase the limit.'
523
                );
524
            }
525
526
            $midnight = $dateTime->setTime(0, 0, 0);
527
            $dateTime = clone $midnight;
528
            $dateTime = $dateTime->modify('-1 minute');
529
            $openingHoursForDay = $this->forDate($dateTime);
530
531
            if ($this->isClosedAt($midnight) && $openingHoursForDay->isOpenAt(Time::fromString('23:59'))) {
532
                return $midnight;
533
            }
534
535
            $previousClose = $openingHoursForDay->previousClose(Time::fromDateTime($dateTime));
536
        }
537
538
        $previousDateTime = $previousClose->toDateTime();
539
540
        return $dateTime->setTime($previousDateTime->format('G'), $previousDateTime->format('i'), 0);
541
    }
542
543
    public function regularClosingDays(): array
544
    {
545
        return array_keys($this->filter(function (OpeningHoursForDay $openingHoursForDay) {
546
            return $openingHoursForDay->isEmpty();
547
        }));
548
    }
549
550
    public function regularClosingDaysISO(): array
551
    {
552
        return Arr::map($this->regularClosingDays(), [Day::class, 'toISO']);
553
    }
554
555
    public function exceptionalClosingDates(): array
556
    {
557
        $dates = array_keys($this->filterExceptions(function (OpeningHoursForDay $openingHoursForDay) {
558
            return $openingHoursForDay->isEmpty();
559
        }));
560
561
        return Arr::map($dates, function ($date) {
562
            return DateTime::createFromFormat('Y-m-d', $date);
563
        });
564
    }
565
566
    public function setTimezone($timezone)
567
    {
568
        $this->timezone = new DateTimeZone($timezone);
569
    }
570
571
    protected function parseOpeningHoursAndExceptions(array $data): array
572
    {
573
        $metaData = Arr::pull($data, 'data', null);
574
        $exceptions = [];
575
        $filters = Arr::pull($data, 'filters', []);
576
        $overflow = (bool) Arr::pull($data, 'overflow', false);
577
578
        foreach (Arr::pull($data, 'exceptions', []) as $key => $exception) {
579
            if (is_callable($exception)) {
580
                $filters[] = $exception;
581
582
                continue;
583
            }
584
585
            $exceptions[$key] = $exception;
586
        }
587
588
        $openingHours = [];
589
590
        foreach ($data as $day => $openingHoursData) {
591
            $openingHours[$this->normalizeDayName($day)] = $openingHoursData;
592
        }
593
594
        return [$openingHours, $exceptions, $metaData, $filters, $overflow];
595
    }
596
597
    protected function setOpeningHoursFromStrings(string $day, array $openingHours)
598
    {
599
        $day = $this->normalizeDayName($day);
600
601
        $data = null;
602
603
        if (isset($openingHours['data'])) {
604
            $data = $openingHours['data'];
605
            unset($openingHours['data']);
606
        }
607
608
        $this->openingHours[$day] = OpeningHoursForDay::fromStrings($openingHours)->setData($data);
609
    }
610
611
    protected function setExceptionsFromStrings(array $exceptions)
612
    {
613
        if (empty($exceptions)) {
614
            return;
615
        }
616
617
        if (! $this->dayLimit) {
618
            $this->dayLimit = 366;
619
        }
620
621
        $this->exceptions = Arr::map($exceptions, function (array $openingHours, string $date) {
622
            $recurring = DateTime::createFromFormat('m-d', $date);
623
624
            if ($recurring === false || $recurring->format('m-d') !== $date) {
625
                $dateTime = DateTime::createFromFormat('Y-m-d', $date);
626
627
                if ($dateTime === false || $dateTime->format('Y-m-d') !== $date) {
628
                    throw InvalidDate::invalidDate($date);
629
                }
630
            }
631
632
            return OpeningHoursForDay::fromStrings($openingHours);
633
        });
634
    }
635
636
    protected function normalizeDayName(string $day)
637
    {
638
        $day = strtolower($day);
639
640
        if (! Day::isValid($day)) {
641
            throw InvalidDayName::invalidDayName($day);
642
        }
643
644
        return $day;
645
    }
646
647
    protected function applyTimezone(DateTimeInterface $date)
648
    {
649
        if ($this->timezone) {
650
            $date = $date->setTimezone($this->timezone);
651
        }
652
653
        return $date;
654
    }
655
656
    public function filter(callable $callback): array
657
    {
658
        return Arr::filter($this->openingHours, $callback);
659
    }
660
661
    public function map(callable $callback): array
662
    {
663
        return Arr::map($this->openingHours, $callback);
664
    }
665
666
    public function flatMap(callable $callback): array
667
    {
668
        return Arr::flatMap($this->openingHours, $callback);
669
    }
670
671
    public function filterExceptions(callable $callback): array
672
    {
673
        return Arr::filter($this->exceptions, $callback);
674
    }
675
676
    public function mapExceptions(callable $callback): array
677
    {
678
        return Arr::map($this->exceptions, $callback);
679
    }
680
681
    public function flatMapExceptions(callable $callback): array
682
    {
683
        return Arr::flatMap($this->exceptions, $callback);
684
    }
685
686
    public function asStructuredData(string $format = 'H:i', $timezone = null): array
687
    {
688 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...
689
            return $openingHoursForDay->map(function (TimeRange $timeRange) use ($format, $timezone, $day) {
690
                return [
691
                    '@type' => 'OpeningHoursSpecification',
692
                    'dayOfWeek' => ucfirst($day),
693
                    'opens' => $timeRange->start()->format($format, $timezone),
694
                    'closes' => $timeRange->end()->format($format, $timezone),
695
                ];
696
            });
697
        });
698
699
        $exceptions = $this->flatMapExceptions(function (OpeningHoursForDay $openingHoursForDay, string $date) use ($format, $timezone) {
700
            if ($openingHoursForDay->isEmpty()) {
701
                $zero = Time::fromString('00:00')->format($format, $timezone);
702
703
                return [[
704
                    '@type' => 'OpeningHoursSpecification',
705
                    'opens' => $zero,
706
                    'closes' => $zero,
707
                    'validFrom' => $date,
708
                    'validThrough' => $date,
709
                ]];
710
            }
711
712 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...
713
                return [
714
                    '@type' => 'OpeningHoursSpecification',
715
                    'opens' => $timeRange->start()->format($format, $timezone),
716
                    'closes' => $timeRange->end()->format($format, $timezone),
717
                    'validFrom' => $date,
718
                    'validThrough' => $date,
719
                ];
720
            });
721
        });
722
723
        return array_merge($regularHours, $exceptions);
724
    }
725
}
726