From 03d2d444c39729bba0502704d1cf80afee1cb574 Mon Sep 17 00:00:00 2001 From: Rhys Laval Date: Tue, 7 May 2019 09:40:23 +0100 Subject: [PATCH] #88 Add the ability to overflow times into the next day (#104) * Add asStructuredData to README usage * Allow on construction for setting a flag for overlapping times. A night club opens that opens till 3am on Friday and Saturday. And modify the API to search for yesterday * Oops, change docs to use overflowing rather than overlapping * Fix styleguide --- README.md | 23 +++++++++- src/OpeningHours.php | 61 +++++++++++++++++++++----- tests/OpeningHoursOverflowTest.php | 69 ++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 tests/OpeningHoursOverflowTest.php diff --git a/README.md b/README.md index de79192..02d2c7d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,17 @@ $openingHours->forDate(new DateTime('2016-12-25')); $openingHours->exceptions(); ``` +On construction you can set a flag for overflowing times across days. For example, for a night club opens that opens till 3am on Friday and Saturday: + + ```php + $openingHours = \Spatie\OpeningHours\OpeningHours::create([ + 'friday' => ['20:00-03:00'], + 'saturday' => ['20:00-03:00'], + ], null, true); + ``` + +This allows the API to further at yesterdays data to check if the opening hours are open from yesterdays time range. + You can add data in definitions then retrieve them: ```php @@ -190,7 +201,7 @@ The package should only be used through the `OpeningHours` class. There are also ### `Spatie\OpeningHours\OpeningHours` -#### `OpeningHours::create(array $data): Spatie\OpeningHours\OpeningHours` +#### `OpeningHours::create(array $data, $timezone = null, bool $overflow = false): Spatie\OpeningHours\OpeningHours` Static factory method to fill the set of opening hours. @@ -203,7 +214,7 @@ $openingHours = OpeningHours::create([ #### `OpeningHours::mergeOverlappingRanges(array $schedule) : array` -For safety sake, creating `OpeningHours` object with overlapping ranges will throw an exception. But you can explicitly merge them. +For safety sake, creating `OpeningHours` object with overlapping ranges will throw an exception. But you can explicitly merge them. This will not cope with overflowing times across days. ``` php $ranges = [ @@ -333,6 +344,14 @@ Returns next close DateTime from the given DateTime $openingHours->nextClose(new DateTime('2016-12-24 11:00:00')); ``` +#### `asStructuredData() : array` + +Returns a (OpeningHoursSpecification)[https://schema.org/openingHoursSpecification] as an array. + +```php +$openingHours->asStructuredData(); +``` + ### `Spatie\OpeningHours\OpeningHoursForDay` This class is meant as read-only. It implements `ArrayAccess`, `Countable` and `IteratorAggregate` so you can process the list of `TimeRange`s in an array-like way. diff --git a/src/OpeningHours.php b/src/OpeningHours.php index 38abcaf..cc5fef9 100644 --- a/src/OpeningHours.php +++ b/src/OpeningHours.php @@ -28,9 +28,19 @@ class OpeningHours /** @var DateTimeZone|null */ protected $timezone = null; - public function __construct($timezone = null) + /** @var bool Allow for overflowing time ranges which overflow into the next day */ + private $overflow; + + public function __construct($timezone = null, bool $overflow = false) { - $this->timezone = $timezone ? new DateTimeZone($timezone) : null; + if ($timezone instanceof DateTimeZone) { + $this->timezone = $timezone; + } elseif (is_string($timezone)) { + $this->timezone = new DateTimeZone($timezone); + } elseif ($timezone) { + throw new \InvalidArgumentException('Invalid Timezone'); + } + $this->overflow = $overflow; $this->openingHours = Day::mapDays(function () { return new OpeningHoursForDay(); @@ -38,13 +48,14 @@ public function __construct($timezone = null) } /** - * @param array $data - * + * @param string[][] $data + * @param string|DateTimeZone|null $timezone + * @param bool $overflow * @return static */ - public static function create(array $data) + public static function create(array $data, $timezone = null, bool $overflow = false): self { - return (new static())->fill($data); + return (new static($timezone, $overflow))->fill($data); } /** @@ -94,13 +105,14 @@ public static function mergeOverlappingRanges(array $data) } /** - * @param array $data - * + * @param string[][] $data + * @param string|DateTimeZone|null $timezone + * @param bool $overflow * @return static */ - public static function createAndMergeOverlappingRanges(array $data) + public static function createAndMergeOverlappingRanges(array $data, $timezone = null, bool $overflow = false) { - return static::create(static::mergeOverlappingRanges($data)); + return static::create(static::mergeOverlappingRanges($data), $timezone, $overflow); } /** @@ -213,6 +225,18 @@ public function isOpenAt(DateTimeInterface $dateTime): bool { $dateTime = $this->applyTimezone($dateTime); + if ($this->overflow) { + $yesterdayDateTime = $dateTime; + if (! ($yesterdayDateTime instanceof DateTimeImmutable)) { + $yesterdayDateTime = clone $yesterdayDateTime; + } + $dateTimeMinus1Day = $yesterdayDateTime->sub(new \DateInterval('P1D')); + $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day); + if ($openingHoursForDayBefore->isOpenAt(Time::fromDateTime($dateTimeMinus1Day))) { + return true; + } + } + $openingHoursForDay = $this->forDate($dateTime); return $openingHoursForDay->isOpenAt(Time::fromDateTime($dateTime)); @@ -272,8 +296,23 @@ public function nextClose(DateTimeInterface $dateTime): DateTimeInterface $dateTime = clone $dateTime; } + $nextClose = null; + if ($this->overflow) { + $yesterday = $dateTime; + if (! ($dateTime instanceof DateTimeImmutable)) { + $yesterday = clone $dateTime; + } + $dateTimeMinus1Day = $yesterday->sub(new \DateInterval('P1D')); + $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day); + if ($openingHoursForDayBefore->isOpenAt(Time::fromDateTime($dateTimeMinus1Day))) { + $nextClose = $openingHoursForDayBefore->nextClose(Time::fromDateTime($dateTime)); + } + } + $openingHoursForDay = $this->forDate($dateTime); - $nextClose = $openingHoursForDay->nextClose(Time::fromDateTime($dateTime)); + if (! $nextClose) { + $nextClose = $openingHoursForDay->nextClose(Time::fromDateTime($dateTime)); + } while ($nextClose === false || $nextClose->hours() >= 24) { $dateTime = $dateTime diff --git a/tests/OpeningHoursOverflowTest.php b/tests/OpeningHoursOverflowTest.php new file mode 100644 index 0000000..e98af2c --- /dev/null +++ b/tests/OpeningHoursOverflowTest.php @@ -0,0 +1,69 @@ + ['09:00-02:00'], + ], null, true); + + $this->assertInstanceOf(TimeRange::class, $openingHours->forDay('monday')[0]); + $this->assertEquals((string) $openingHours->forDay('monday')[0], '09:00-02:00'); + } + + /** @test */ + public function check_open_with_overflow() + { + $openingHours = OpeningHours::create([ + 'monday' => ['09:00-02:00'], + ], null, true); + + $shouldBeOpen = new DateTime('2019-04-23 01:00:00'); + $this->assertTrue($openingHours->isOpenAt($shouldBeOpen)); + } + + /** @test */ + public function check_open_with_overflow_immutable() + { + $openingHours = OpeningHours::create([ + 'monday' => ['09:00-02:00'], + ], null, true); + + $shouldBeOpen = new DateTimeImmutable('2019-04-23 01:00:00'); + $this->assertTrue($openingHours->isOpenAt($shouldBeOpen)); + } + + /** @test */ + public function next_close_with_overflow() + { + $openingHours = OpeningHours::create([ + 'monday' => ['09:00-02:00'], + ], null, true); + + $shouldBeOpen = new DateTime('2019-04-23 01:00:00'); + $this->assertEquals('2019-04-23 02:00:00', $openingHours->nextClose($shouldBeOpen)->format('Y-m-d H:i:s')); + } + + /** @test */ + public function next_close_with_overflow_immutable() + { + $openingHours = OpeningHours::create([ + 'monday' => ['09:00-02:00'], + ], null, true); + + $shouldBeOpen = new DateTimeImmutable('2019-04-23 01:00:00'); + $nextTimeClosed = $openingHours->nextClose($shouldBeOpen)->format('Y-m-d H:i:s'); + $this->assertEquals('2019-04-23 02:00:00', $nextTimeClosed); + $this->assertEquals('2019-04-23 01:00:00', $shouldBeOpen->format('Y-m-d H:i:s')); + } +}