From 6f0430a76ed3c517ef9f157c1f65f6784555c6e2 Mon Sep 17 00:00:00 2001 From: KyleKatarn Date: Sat, 25 Nov 2023 15:39:20 +0100 Subject: [PATCH 1/3] - Allow empty array for createFromStructuredData() - Make OpeningHoursSpecificationParser constructor private and add separated named constructors --- src/OpeningHours.php | 8 ++-- src/OpeningHoursSpecificationParser.php | 51 ++++++++++++++----------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/OpeningHours.php b/src/OpeningHours.php index 97c4699..2c47f26 100644 --- a/src/OpeningHours.php +++ b/src/OpeningHours.php @@ -128,9 +128,11 @@ public static function createFromStructuredData( string|DateTimeZone|null $timezone = null, string|DateTimeZone|null $outputTimezone = null, ): self { - $parser = new OpeningHoursSpecificationParser($structuredData); - - return new static($parser->getOpeningHours(), $timezone, $outputTimezone); + return new static( + OpeningHoursSpecificationParser::create($structuredData)->getOpeningHours(), + $timezone, + $outputTimezone, + ); } /** diff --git a/src/OpeningHoursSpecificationParser.php b/src/OpeningHoursSpecificationParser.php index 5e6fc78..918e7cd 100644 --- a/src/OpeningHoursSpecificationParser.php +++ b/src/OpeningHoursSpecificationParser.php @@ -11,29 +11,8 @@ final class OpeningHoursSpecificationParser { private array $openingHours = []; - public function __construct(array|string|null $openingHoursSpecification) + private function __construct(array $openingHoursSpecification) { - if (is_string($openingHoursSpecification)) { - try { - $openingHoursSpecification = json_decode( - $openingHoursSpecification, - true, - flags: JSON_THROW_ON_ERROR, - ); - } catch (JsonException $e) { - throw new InvalidOpeningHoursSpecification( - 'Invalid https://schema.org/OpeningHoursSpecification JSON', - previous: $e, - ); - } - } - - if (! is_array($openingHoursSpecification) || $openingHoursSpecification === []) { - throw new InvalidOpeningHoursSpecification( - 'Invalid https://schema.org/OpeningHoursSpecification structured data', - ); - } - foreach ($openingHoursSpecification as $openingHoursSpecificationItem) { if (isset($openingHoursSpecificationItem['dayOfWeek'])) { /* @@ -83,6 +62,34 @@ public function __construct(array|string|null $openingHoursSpecification) } } + public static function createFromArray(array $openingHoursSpecification): self + { + return new self($openingHoursSpecification); + } + + public static function createFromString(string $openingHoursSpecification): self + { + try { + return self::createFromArray(json_decode( + $openingHoursSpecification, + true, + flags: JSON_THROW_ON_ERROR, + )); + } catch (JsonException $e) { + throw new InvalidOpeningHoursSpecification( + 'Invalid https://schema.org/OpeningHoursSpecification JSON', + previous: $e, + ); + } + } + + public static function create(array|string $openingHoursSpecification): self + { + return is_string($openingHoursSpecification) + ? self::createFromString($openingHoursSpecification) + : self::createFromArray($openingHoursSpecification); + } + public function getOpeningHours(): array { return $this->openingHours; From d3f3ab77933d9ca9cc542c1cbfd958945b1b1802 Mon Sep 17 00:00:00 2001 From: KyleKatarn Date: Sat, 25 Nov 2023 17:21:20 +0100 Subject: [PATCH 2/3] - Add details in exceptions to know what is the issue precisely and which item is incorrect - Accept empty array as per specs for structured-data - Add utils to know if opening-hours are always open/closed - Add OpeningHours::every() method --- README.md | 25 +++ src/OpeningHours.php | 35 ++- src/OpeningHoursSpecificationParser.php | 176 +++++++++------ tests/OpeningHoursFillTest.php | 1 - tests/OpeningHoursSpecificationParserTest.php | 209 +++++++++++++++++- tests/OpeningHoursTest.php | 3 +- 6 files changed, 375 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index b729708..a490e0d 100644 --- a/README.md +++ b/README.md @@ -421,6 +421,31 @@ Checks if the business is closed right now. $openingHours->isClosed(); ``` +#### `OpeningHours::isAlwaysOpen(): bool` + +Checks if the business is open 24/7, has no exceptions and no filters. + +```php +if ($openingHours->isAlwaysOpen()) { + echo 'This business is open all day long every day.'; +} +``` + +#### `OpeningHours::isAlwaysClosed(): bool` + +Checks if the business is never open, has no exceptions and no filters. + +`OpeningHours` accept empty array or list with every week day empty with no prejudices. + +If it's not a valid state in your domain, you should use this method to throw an exception +or show an error. + +```php +if ($openingHours->isAlwaysClosed()) { + throw new RuntimeException('Opening hours missing'); +} +``` + #### `OpeningHours::nextOpen` ```php diff --git a/src/OpeningHours.php b/src/OpeningHours.php index 2c47f26..3f3f053 100644 --- a/src/OpeningHours.php +++ b/src/OpeningHours.php @@ -20,7 +20,6 @@ use Spatie\OpeningHours\Helpers\DataTrait; use Spatie\OpeningHours\Helpers\DateTimeCopier; use Spatie\OpeningHours\Helpers\DiffTrait; -use ValueError; class OpeningHours { @@ -215,7 +214,7 @@ public static function isValid(array $data): bool static::create($data); return true; - } catch (Exception|ValueError) { + } catch (Exception) { return false; } } @@ -893,6 +892,11 @@ protected function getDateWithTimezone(DateTimeInterface $date, ?DateTimeZone $t return $date; } + /** + * Returns opening hours for the days that match a given condition as an array. + * + * @return OpeningHoursForDay[] + */ public function filter(callable $callback): array { return Arr::filter($this->openingHours, $callback); @@ -908,6 +912,11 @@ public function flatMap(callable $callback): array return Arr::flatMap($this->openingHours, $callback); } + /** + * Returns opening hours for the exceptions that match a given condition as an array. + * + * @return OpeningHoursForDay[] + */ public function filterExceptions(callable $callback): array { return Arr::filter($this->exceptions, $callback); @@ -923,6 +932,14 @@ public function flatMapExceptions(callable $callback): array return Arr::flatMap($this->exceptions, $callback); } + /** Checks that opening hours for every day of the week matches a given condition */ + public function every(callable $callback): bool + { + return $this->filter( + static fn (OpeningHoursForDay $day) => !$callback($day), + ) === []; + } + public function asStructuredData( string $format = TimeDataContainer::TIME_FORMAT, DateTimeZone|string|null $timezone = null, @@ -967,6 +984,20 @@ static function (OpeningHoursForDay $openingHoursForDay, string $date) use ($for return array_merge($regularHours, $exceptions); } + public function isAlwaysClosed(): bool + { + return $this->exceptions === [] && $this->filters === [] && $this->every( + static fn (OpeningHoursForDay $day) => $day->isEmpty(), + ); + } + + public function isAlwaysOpen(): bool + { + return $this->exceptions === [] && $this->filters === [] && $this->every( + static fn (OpeningHoursForDay $day) => ((string) $day) === '00:00-24:00', + ); + } + private static function filterHours(array $data, array $excludedKeys): Generator { foreach ($data as $key => $value) { diff --git a/src/OpeningHoursSpecificationParser.php b/src/OpeningHoursSpecificationParser.php index 918e7cd..290e746 100644 --- a/src/OpeningHoursSpecificationParser.php +++ b/src/OpeningHoursSpecificationParser.php @@ -13,50 +13,15 @@ final class OpeningHoursSpecificationParser private function __construct(array $openingHoursSpecification) { - foreach ($openingHoursSpecification as $openingHoursSpecificationItem) { - if (isset($openingHoursSpecificationItem['dayOfWeek'])) { - /* - * Regular opening hours - */ - $dayOfWeek = $openingHoursSpecificationItem['dayOfWeek']; - if (is_array($dayOfWeek)) { - // Multiple days of week for same specification - foreach ($dayOfWeek as $dayOfWeekItem) { - $this->addDayOfWeekHours( - $dayOfWeekItem, - $openingHoursSpecificationItem['opens'] ?? null, - $openingHoursSpecificationItem['closes'] ?? null, - ); - } - } elseif (is_string($dayOfWeek)) { - $this->addDayOfWeekHours( - $dayOfWeek, - $openingHoursSpecificationItem['opens'] ?? null, - $openingHoursSpecificationItem['closes'] ?? null, - ); - } else { - throw new InvalidOpeningHoursSpecification( - 'Invalid https://schema.org/OpeningHoursSpecification structured data', - ); - } - } elseif ( - isset($openingHoursSpecificationItem['validFrom']) && - isset($openingHoursSpecificationItem['validThrough']) - ) { - /* - * Exception opening hours - */ - $validFrom = $openingHoursSpecificationItem['validFrom']; - $validThrough = $openingHoursSpecificationItem['validThrough']; - $this->addExceptionsHours( - $validFrom, - $validThrough, - $openingHoursSpecificationItem['opens'] ?? null, - $openingHoursSpecificationItem['closes'] ?? null, - ); - } else { + foreach ($openingHoursSpecification as $index => $openingHoursSpecificationItem) { + try { + $this->parseOpeningHoursSpecificationItem($openingHoursSpecificationItem); + } catch (InvalidOpeningHoursSpecification $exception) { + $message = $exception->getMessage(); + throw new InvalidOpeningHoursSpecification( - 'Invalid https://schema.org/OpeningHoursSpecification structured data', + "Invalid openingHoursSpecification item at index $index: $message", + previous: $exception, ); } } @@ -95,6 +60,26 @@ public function getOpeningHours(): array return $this->openingHours; } + /** + * Regular opening hours + */ + private function addDaysOfWeek( + array $dayOfWeek, + mixed $opens, + mixed $closes, + ): void { + // Multiple days of week for same specification + foreach ($dayOfWeek as $dayOfWeekItem) { + if (! is_string($dayOfWeekItem)) { + throw new InvalidOpeningHoursSpecification( + 'Invalid https://schema.org/OpeningHoursSpecification dayOfWeek', + ); + } + + $this->addDayOfWeekHours($dayOfWeekItem, $opens, $closes); + } + } + private function schemaOrgDayToString(string $schemaOrgDaySpec): string { // Support official and Google-flavored Day specifications @@ -106,14 +91,19 @@ private function schemaOrgDayToString(string $schemaOrgDaySpec): string 'Friday', 'https://schema.org/Friday' => 'friday', 'Saturday', 'https://schema.org/Saturday' => 'saturday', 'Sunday', 'https://schema.org/Sunday' => 'sunday', - default => throw new InvalidOpeningHoursSpecification('Invalid https://schema.org Day specification'), + 'PublicHolidays', 'https://schema.org/PublicHolidays' => throw new InvalidOpeningHoursSpecification( + 'PublicHolidays not supported', + ), + default => throw new InvalidOpeningHoursSpecification( + 'Invalid https://schema.org Day specification', + ), }; } private function addDayOfWeekHours( string $dayOfWeek, - ?string $opens, - ?string $closes, + mixed $opens, + mixed $closes, ): void { $dayOfWeek = self::schemaOrgDayToString($dayOfWeek); @@ -127,20 +117,17 @@ private function addDayOfWeekHours( } private function addExceptionsHours( - ?string $validFrom, - ?string $validThrough, - ?string $opens, - ?string $closes + string $validFrom, + string $validThrough, + mixed $opens, + mixed $closes, ): void { - if (! is_string($validFrom) || ! is_string($validThrough)) { - throw new InvalidOpeningHoursSpecification('Missing validFrom and validThrough dates'); + if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $validFrom)) { + throw new InvalidOpeningHoursSpecification('Invalid validFrom date'); } - if ( - ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $validFrom) || - ! preg_match('/^\d{4}-\d{2}-\d{2}$/', $validThrough) - ) { - throw new InvalidOpeningHoursSpecification('Invalid validFrom and validThrough dates'); + if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $validThrough)) { + throw new InvalidOpeningHoursSpecification('Invalid validThrough date'); } $exceptionKey = $validFrom === $validThrough ? $validFrom : $validFrom.' to '.$validThrough; @@ -158,19 +145,24 @@ private function addExceptionsHours( $this->openingHours['exceptions'][$exceptionKey][] = $hours; } - private function formatHours( - ?string $opens, - ?string $closes, - ): ?string { - if (! is_string($opens) || ! is_string($closes)) { - throw new InvalidOpeningHoursSpecification('Missing opens and closes hours'); + private function formatHours(mixed $opens, mixed $closes): ?string + { + if ($opens === null) { + if ($closes !== null) { + throw new InvalidOpeningHoursSpecification( + 'Property opens and closes must be both null or both string', + ); + } + + return null; + } + + if (! is_string($opens) || ! preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $opens)) { + throw new InvalidOpeningHoursSpecification('Invalid opens hour'); } - if ( - ! preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $opens) || - ! preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $closes) - ) { - throw new InvalidOpeningHoursSpecification('Invalid opens and closes hours'); + if (! is_string($closes) || ! preg_match('/^\d{2}:\d{2}(:\d{2})?$/', $closes)) { + throw new InvalidOpeningHoursSpecification('Invalid closes hours'); } // strip seconds part if present @@ -182,6 +174,52 @@ private function formatHours( return null; } - return $opens.'-'.$closes; + return $opens.'-'.($closes === '23:59' ? '24:00' : $closes); + } + + private function parseOpeningHoursSpecificationItem(mixed $openingHoursSpecificationItem): void + { + // extract $openingHoursSpecificationItem keys into variables + [ + 'dayOfWeek' => $dayOfWeek, + 'validFrom' => $validFrom, + 'validThrough' => $validThrough, + 'opens' => $opens, + 'closes' => $closes, + ] = array_merge([ + // Default values: + 'dayOfWeek' => null, + 'validFrom' => null, + 'validThrough' => null, + 'opens' => null, + 'closes' => null, + ], $openingHoursSpecificationItem); + + if ($dayOfWeek !== null) { + if (is_string($dayOfWeek)) { + $dayOfWeek = [$dayOfWeek]; + } + + if (! is_array($dayOfWeek)) { + throw new InvalidOpeningHoursSpecification( + 'Property dayOfWeek must be a string or an array of strings', + ); + } + + $this->addDaysOfWeek($dayOfWeek, $opens, $closes); + + return; + } + + if (! is_string($validFrom) || ! is_string($validThrough)) { + throw new InvalidOpeningHoursSpecification( + 'Contains neither dayOfWeek nor validFrom and validThrough dates', + ); + } + + /* + * Exception opening hours + */ + $this->addExceptionsHours($validFrom, $validThrough, $opens, $closes); } } diff --git a/tests/OpeningHoursFillTest.php b/tests/OpeningHoursFillTest.php index e89a4b8..99c8ffc 100644 --- a/tests/OpeningHoursFillTest.php +++ b/tests/OpeningHoursFillTest.php @@ -11,7 +11,6 @@ use Spatie\OpeningHours\OpeningHours; use Spatie\OpeningHours\OpeningHoursForDay; use Spatie\OpeningHours\TimeRange; -use ValueError; class OpeningHoursFillTest extends TestCase { diff --git a/tests/OpeningHoursSpecificationParserTest.php b/tests/OpeningHoursSpecificationParserTest.php index 10fb471..b10a7cc 100644 --- a/tests/OpeningHoursSpecificationParserTest.php +++ b/tests/OpeningHoursSpecificationParserTest.php @@ -4,7 +4,10 @@ namespace Spatie\OpeningHours\Test; +use DateTimeImmutable; use PHPUnit\Framework\TestCase; +use Spatie\OpeningHours\Day; +use Spatie\OpeningHours\Exceptions\InvalidOpeningHoursSpecification; use Spatie\OpeningHours\OpeningHours; class OpeningHoursSpecificationParserTest extends TestCase @@ -68,7 +71,7 @@ public function testCreateFromStructuredData(): void ] JSON; - $openingHours = OpeningHours::createFromStructuredData(json_decode($openingHoursSpecs, true)); + $openingHours = OpeningHours::createFromStructuredData($openingHoursSpecs); $this->assertInstanceOf(OpeningHours::class, $openingHours); $this->assertCount(2, $openingHours->forDay('monday')); @@ -100,4 +103,208 @@ public function testCreateFromStructuredData(): void 'Opened on 2023 Sunday before Christmas day', ); } + + public function testEmptySpecs(): void + { + $openingHours = OpeningHours::createFromStructuredData([]); + + $this->assertTrue($openingHours->isAlwaysClosed()); + } + + public function testH24Specs(): void + { + $openingHours = OpeningHours::createFromStructuredData([ + [ + 'opens' => '00:00', + 'closes' => '23:59', + 'dayOfWeek' => [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + ], + ], + ]); + + $this->assertTrue( + $openingHours->isOpenAt(new DateTimeImmutable('2023-11-27 23:59:34')), + 'As per specs, 23:59 is assumed to mean until end of day', + ); + $this->assertFalse( + $openingHours->isOpenAt(new DateTimeImmutable('2023-11-25 23:59:34')), + 'Saturday and Sunday not specified means they are closed', + ); + $this->assertFalse( + $openingHours->isAlwaysOpen(), + 'Saturday and Sunday not specified means they are closed', + ); + + $openingHours = OpeningHours::createFromStructuredData([ + [ + 'opens' => '00:00', + 'closes' => '23:59', + 'dayOfWeek' => [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ], + ], + ]); + + $this->assertTrue( + $openingHours->isAlwaysOpen(), + 'As per specs, 23:59 is assumed to mean until end of day', + ); + } + + public function testClosedDay(): void + { + $openingHours = OpeningHours::createFromStructuredData([ + ['dayOfWeek' => 'Monday'], + ]); + + $this->assertSame('', (string) $openingHours->forDay(Day::MONDAY)); + } + + public function testInvalidJson(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid https://schema.org/OpeningHoursSpecification JSON', + )); + + OpeningHours::createFromStructuredData('{'); + } + + public function testInvalidDayOfWeek(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 1: Property dayOfWeek must be a string or an array of strings', + )); + + OpeningHours::createFromStructuredData([ + ['dayOfWeek' => []], + ['dayOfWeek' => true], + ]); + } + + public function testInvalidDayType(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Invalid https://schema.org/OpeningHoursSpecification dayOfWeek', + )); + + OpeningHours::createFromStructuredData([ + ['dayOfWeek' => [true]], + ]); + } + + public function testInvalidDayName(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Invalid https://schema.org Day specification', + )); + + OpeningHours::createFromStructuredData([ + ['dayOfWeek' => ['Wedmonday']], + ]); + } + + public function testUnsupportedPublicHolidays(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: PublicHolidays not supported', + )); + + OpeningHours::createFromStructuredData([ + ['dayOfWeek' => 'PublicHolidays'], + ]); + } + + public function testInvalidValidPair(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Contains neither dayOfWeek nor validFrom and validThrough dates', + )); + + OpeningHours::createFromStructuredData([ + ['validFrom' => '2023-11-25'], + ]); + } + + public function testInvalidOpens(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Invalid opens hour', + )); + + OpeningHours::createFromStructuredData([ + [ + 'dayOfWeek' => 'Monday', + 'opens' => 'noon', + 'closes' => '14:00', + ], + ]); + } + + public function testInvalidCloses(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Invalid closes hour', + )); + + OpeningHours::createFromStructuredData([ + [ + 'dayOfWeek' => 'Monday', + 'opens' => '10:00', + 'closes' => 'noon', + ], + ]); + } + + public function testClosesOnly(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Property opens and closes must be both null or both string', + )); + + OpeningHours::createFromStructuredData([ + [ + 'dayOfWeek' => 'Monday', + 'closes' => '10:00', + ], + ]); + } + + public function testInvalidValidFrom(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Invalid validFrom date', + )); + + OpeningHours::createFromStructuredData([ + [ + 'validFrom' => '11/11/2023', + 'validThrough' => '2023-11-25', + ], + ]); + } + + public function testInvalidValidThrough(): void + { + self::expectExceptionObject(new InvalidOpeningHoursSpecification( + 'Invalid openingHoursSpecification item at index 0: Invalid validThrough date', + )); + + OpeningHours::createFromStructuredData([ + [ + 'validFrom' => '2023-11-11', + 'validThrough' => '25/11/20235', + ], + ]); + } } diff --git a/tests/OpeningHoursTest.php b/tests/OpeningHoursTest.php index 87dbcb1..a9ce3b1 100644 --- a/tests/OpeningHoursTest.php +++ b/tests/OpeningHoursTest.php @@ -957,7 +957,8 @@ public function it_can_determine_that_its_open_now() /** @test */ public function it_can_use_day_enum() { - $openingHours = new class () extends OpeningHours { + $openingHours = new class () extends OpeningHours + { public readonly array $days; public function __construct() From 793b060dd9d384e6e3e51c4bac5b7e328d44e152 Mon Sep 17 00:00:00 2001 From: KyleKatarn Date: Sat, 25 Nov 2023 17:29:17 +0100 Subject: [PATCH 3/3] - Allow overflow for structured-data --- src/OpeningHours.php | 8 ++++++-- src/OpeningHoursSpecificationParser.php | 2 +- tests/OpeningHoursSpecificationParserTest.php | 17 +++++++++++++++++ tests/OpeningHoursTest.php | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/OpeningHours.php b/src/OpeningHours.php index 3f3f053..6525f5b 100644 --- a/src/OpeningHours.php +++ b/src/OpeningHours.php @@ -128,7 +128,11 @@ public static function createFromStructuredData( string|DateTimeZone|null $outputTimezone = null, ): self { return new static( - OpeningHoursSpecificationParser::create($structuredData)->getOpeningHours(), + array_merge( + // https://schema.org/OpeningHoursSpecification allows overflow by default + ['overflow' => true], + OpeningHoursSpecificationParser::create($structuredData)->getOpeningHours(), + ), $timezone, $outputTimezone, ); @@ -936,7 +940,7 @@ public function flatMapExceptions(callable $callback): array public function every(callable $callback): bool { return $this->filter( - static fn (OpeningHoursForDay $day) => !$callback($day), + static fn (OpeningHoursForDay $day) => ! $callback($day), ) === []; } diff --git a/src/OpeningHoursSpecificationParser.php b/src/OpeningHoursSpecificationParser.php index 290e746..fabd81c 100644 --- a/src/OpeningHoursSpecificationParser.php +++ b/src/OpeningHoursSpecificationParser.php @@ -61,7 +61,7 @@ public function getOpeningHours(): array } /** - * Regular opening hours + * Regular opening hours. */ private function addDaysOfWeek( array $dayOfWeek, diff --git a/tests/OpeningHoursSpecificationParserTest.php b/tests/OpeningHoursSpecificationParserTest.php index b10a7cc..703a806 100644 --- a/tests/OpeningHoursSpecificationParserTest.php +++ b/tests/OpeningHoursSpecificationParserTest.php @@ -111,6 +111,23 @@ public function testEmptySpecs(): void $this->assertTrue($openingHours->isAlwaysClosed()); } + public function testRangeOverNight(): void + { + $openingHours = OpeningHours::createFromStructuredData([ + [ + 'dayOfWeek' => 'Monday', + 'opens' => '18:00', + 'closes' => '02:00', + ], + ]); + + $this->assertTrue($openingHours->isClosedAt(new DateTimeImmutable('2023-11-27 17:50'))); + $this->assertTrue($openingHours->isOpenAt(new DateTimeImmutable('2023-11-27 23:55'))); + $this->assertTrue($openingHours->isOpenAt(new DateTimeImmutable('2023-11-27 23:59:59.99'))); + $this->assertTrue($openingHours->isOpenAt(new DateTimeImmutable('2023-11-28 01:50'))); + $this->assertTrue($openingHours->isClosedAt(new DateTimeImmutable('2023-11-28 19:00'))); + } + public function testH24Specs(): void { $openingHours = OpeningHours::createFromStructuredData([ diff --git a/tests/OpeningHoursTest.php b/tests/OpeningHoursTest.php index a9ce3b1..71a9588 100644 --- a/tests/OpeningHoursTest.php +++ b/tests/OpeningHoursTest.php @@ -957,7 +957,7 @@ public function it_can_determine_that_its_open_now() /** @test */ public function it_can_use_day_enum() { - $openingHours = new class () extends OpeningHours + $openingHours = new class extends OpeningHours { public readonly array $days;