diff --git a/README.md b/README.md index 6ba683f..bf352ef 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,7 @@ Returns an `OpeningHoursForDay` object for a regular day. A day is lowercase str $openingHours->forDay('monday'); ``` -#### `OpeningHours::forDate(DateTime $dateTime): Spatie\OpeningHours\OpeningHoursForDay` +#### `OpeningHours::forDate(DateTimeInterface $dateTime): Spatie\OpeningHours\OpeningHoursForDay` Returns an `OpeningHoursForDay` object for a specific date. It looks for an exception on that day, and otherwise it returns the opening hours based on the regular schedule. @@ -400,33 +400,90 @@ Checks if the business is closed right now. $openingHours->isClosed(); ``` -#### `OpeningHours::nextOpen(DateTimeInterface $dateTime) : DateTime` +#### `OpeningHours::nextOpen` -Returns next open DateTime from the given DateTime +```php +OpeningHours::nextOpen( + ?DateTimeInterface $dateTime = null, + ?DateTimeInterface $searchUntil = null, + ?DateTimeInterface $cap = null, +) : DateTimeInterface` +``` + +Returns next open `DateTime` from the given `DateTime` (`$dateTime` or from now if this parameter is null or omitted). + +If a `DateTimeImmutable` object is passed, a `DateTimeImmutable` object is returned. + +Set `$searchUntil` to a date to throw an exception if no open time can be found before this moment. + +Set `$cap` to a date so if no open time can be found before this moment, `$cap` is returned. ```php $openingHours->nextOpen(new DateTime('2016-12-24 11:00:00')); ``` +` + +#### `OpeningHours::nextClose` + +```php +OpeningHours::nextClose( + ?DateTimeInterface $dateTime = null, + ?DateTimeInterface $searchUntil = null, + ?DateTimeInterface $cap = null, +) : DateTimeInterface` +``` + +Returns next close `DateTime` from the given `DateTime` (`$dateTime` or from now if this parameter is null or omitted). + +If a `DateTimeImmutable` object is passed, a `DateTimeImmutable` object is returned. -#### `OpeningHours::nextClose(DateTimeInterface $dateTime) : DateTime` +Set `$searchUntil` to a date to throw an exception if no closed time can be found before this moment. -Returns next close DateTime from the given DateTime +Set `$cap` to a date so if no closed time can be found before this moment, `$cap` is returned. ```php $openingHours->nextClose(new DateTime('2016-12-24 11:00:00')); ``` -#### `OpeningHours::previousOpen(DateTimeInterface $dateTime) : DateTime` +#### `OpeningHours::previousOpen` -Returns previous open DateTime from the given DateTime +```php +OpeningHours::previousOpen( + ?DateTimeInterface $dateTime = null, + ?DateTimeInterface $searchUntil = null, + ?DateTimeInterface $cap = null, +) : DateTimeInterface` +``` + +Returns previous open `DateTime` from the given `DateTime` (`$dateTime` or from now if this parameter is null or omitted). + +If a `DateTimeImmutable` object is passed, a `DateTimeImmutable` object is returned. + +Set `$searchUntil` to a date to throw an exception if no open time can be found after this moment. + +Set `$cap` to a date so if no open time can be found after this moment, `$cap` is returned. ```php $openingHours->previousOpen(new DateTime('2016-12-24 11:00:00')); ``` -#### `OpeningHours::previousClose(DateTimeInterface $dateTime) : DateTime` +#### `OpeningHours::previousClose` + +```php +OpeningHours::previousClose( + ?DateTimeInterface $dateTime = null, + ?DateTimeInterface $searchUntil = null, + ?DateTimeInterface $cap = null, +) : DateTimeInterface` +``` + +Returns previous close `DateTime` from the given `DateTime` (`$dateTime` or from now if this parameter is null or omitted). + +If a `DateTimeImmutable` object is passed, a `DateTimeImmutable` object is returned. + +Set `$searchUntil` to a date to throw an exception if no closed time can be found after this moment. -Returns previous close DateTime from the given DateTime +Set `$cap` to a date so if no closed time can be found after this moment, `$cap` is returned. ```php $openingHours->nextClose(new DateTime('2016-12-24 11:00:00')); diff --git a/src/Exceptions/SearchLimitReached.php b/src/Exceptions/SearchLimitReached.php new file mode 100644 index 0000000..96b4aca --- /dev/null +++ b/src/Exceptions/SearchLimitReached.php @@ -0,0 +1,13 @@ +format('Y-m-d H:i:s.u e')); + } +} diff --git a/src/Helpers/DiffTrait.php b/src/Helpers/DiffTrait.php index 9b79a09..82d0335 100644 --- a/src/Helpers/DiffTrait.php +++ b/src/Helpers/DiffTrait.php @@ -18,11 +18,12 @@ private function diffInSeconds(string $stateCheckMethod, string $nextDateMethod, while ($date < $endDate) { if ($this->$stateCheckMethod($date)) { - $date = $this->$skipDateMethod($date); + $date = $this->$skipDateMethod($date, null, $endDate); + continue; } - $nextDate = min($endDate, $this->$nextDateMethod($date)); + $nextDate = min($endDate, $this->$nextDateMethod($date, null, $endDate)); $time += floatval($nextDate->format('U.u')) - floatval($date->format('U.u')); $date = $nextDate; } diff --git a/src/OpeningHours.php b/src/OpeningHours.php index 3dac76f..f8217b4 100644 --- a/src/OpeningHours.php +++ b/src/OpeningHours.php @@ -13,6 +13,7 @@ use Spatie\OpeningHours\Exceptions\InvalidDayName; use Spatie\OpeningHours\Exceptions\InvalidTimezone; use Spatie\OpeningHours\Exceptions\MaximumLimitExceeded; +use Spatie\OpeningHours\Exceptions\SearchLimitReached; use Spatie\OpeningHours\Helpers\Arr; use Spatie\OpeningHours\Helpers\DataTrait; use Spatie\OpeningHours\Helpers\DateTimeCopier; @@ -24,7 +25,7 @@ class OpeningHours use DataTrait, DateTimeCopier, DiffTrait; - /** @var \Spatie\OpeningHours\Day[] */ + /** @var \Spatie\OpeningHours\OpeningHoursForDay[] */ protected $openingHours = []; /** @var \Spatie\OpeningHours\OpeningHoursForDay[] */ @@ -453,8 +454,11 @@ public function currentOpenRangeEnd(DateTimeInterface $dateTime) ); } - public function nextOpen(DateTimeInterface $dateTime = null): DateTimeInterface - { + public function nextOpen( + DateTimeInterface $dateTime = null, + DateTimeInterface $searchUntil = null, + DateTimeInterface $cap = null + ): DateTimeInterface { $outputTimezone = $this->getOutputTimezone($dateTime); $dateTime = $this->applyTimezone($dateTime ?? new $this->dateTimeClass()); $dateTime = $this->copyDateTime($dateTime); @@ -478,6 +482,14 @@ public function nextOpen(DateTimeInterface $dateTime = null): DateTimeInterface return $this->getDateWithTimezone($dateTime, $outputTimezone); } + if ($cap && $dateTime > $cap) { + return $cap; + } + + if ($searchUntil && $dateTime > $searchUntil) { + throw SearchLimitReached::forDate($searchUntil); + } + $openingHoursForDay = $this->forDate($dateTime); $nextOpen = $openingHoursForDay->nextOpen(PreciseTime::fromDateTime($dateTime)); @@ -498,21 +510,27 @@ public function nextOpen(DateTimeInterface $dateTime = null): DateTimeInterface ); } - public function nextClose(DateTimeInterface $dateTime = null): DateTimeInterface - { + public function nextClose( + DateTimeInterface $dateTime = null, + DateTimeInterface $searchUntil = null, + DateTimeInterface $cap = null + ): DateTimeInterface { $outputTimezone = $this->getOutputTimezone($dateTime); $dateTime = $this->applyTimezone($dateTime ?? new $this->dateTimeClass()); $dateTime = $this->copyDateTime($dateTime); $nextClose = null; + if ($this->overflow) { $dateTimeMinus1Day = $this->copyDateTime($dateTime)->modify('-1 day'); $openingHoursForDayBefore = $this->forDate($dateTimeMinus1Day); + if ($openingHoursForDayBefore->isOpenAtNight(PreciseTime::fromDateTime($dateTimeMinus1Day))) { $nextClose = $openingHoursForDayBefore->nextClose(PreciseTime::fromDateTime($dateTime)); } } $openingHoursForDay = $this->forDate($dateTime); + if (! $nextClose) { $nextClose = $openingHoursForDay->nextClose(PreciseTime::fromDateTime($dateTime)); @@ -539,6 +557,14 @@ public function nextClose(DateTimeInterface $dateTime = null): DateTimeInterface return $this->getDateWithTimezone($dateTime, $outputTimezone); } + if ($cap && $dateTime > $cap) { + return $cap; + } + + if ($searchUntil && $dateTime > $searchUntil) { + throw SearchLimitReached::forDate($searchUntil); + } + $openingHoursForDay = $this->forDate($dateTime); $nextClose = $openingHoursForDay->nextClose(PreciseTime::fromDateTime($dateTime)); @@ -552,8 +578,11 @@ public function nextClose(DateTimeInterface $dateTime = null): DateTimeInterface ); } - public function previousOpen(DateTimeInterface $dateTime): DateTimeInterface - { + public function previousOpen( + DateTimeInterface $dateTime, + DateTimeInterface $searchUntil = null, + DateTimeInterface $cap = null + ): DateTimeInterface { $outputTimezone = $this->getOutputTimezone($dateTime); $dateTime = $this->copyDateTime($this->applyTimezone($dateTime)); $openingHoursForDay = $this->forDate($dateTime); @@ -578,6 +607,14 @@ public function previousOpen(DateTimeInterface $dateTime): DateTimeInterface return $this->getDateWithTimezone($midnight, $outputTimezone); } + if ($cap && $dateTime < $cap) { + return $cap; + } + + if ($searchUntil && $dateTime < $searchUntil) { + throw SearchLimitReached::forDate($searchUntil); + } + $previousOpen = $openingHoursForDay->previousOpen(PreciseTime::fromDateTime($dateTime)); } @@ -589,8 +626,11 @@ public function previousOpen(DateTimeInterface $dateTime): DateTimeInterface ); } - public function previousClose(DateTimeInterface $dateTime): DateTimeInterface - { + public function previousClose( + DateTimeInterface $dateTime, + DateTimeInterface $searchUntil = null, + DateTimeInterface $cap = null + ): DateTimeInterface { $outputTimezone = $this->getOutputTimezone($dateTime); $dateTime = $this->copyDateTime($this->applyTimezone($dateTime)); $previousClose = null; @@ -626,6 +666,14 @@ public function previousClose(DateTimeInterface $dateTime): DateTimeInterface return $this->getDateWithTimezone($midnight, $outputTimezone); } + if ($cap && $dateTime < $cap) { + return $cap; + } + + if ($searchUntil && $dateTime < $searchUntil) { + throw SearchLimitReached::forDate($searchUntil); + } + $previousClose = $openingHoursForDay->previousClose(PreciseTime::fromDateTime($dateTime)); } diff --git a/src/Time.php b/src/Time.php index 66653c4..0b263e7 100644 --- a/src/Time.php +++ b/src/Time.php @@ -99,17 +99,14 @@ public function toDateTime(DateTimeInterface $date = null): DateTimeInterface public function format(string $format = 'H:i', $timezone = null): string { $date = $timezone - ? new DateTime('1970-01-01 00:00:00', $timezone instanceof DateTimeZone + ? new DateTimeImmutable('1970-01-01 00:00:00', $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone) ) : null; if ($this->hours === 24 && $this->minutes === 0 && substr($format, 0, 3) === 'H:i') { - return '24:00'.(strlen($format) > 3 - ? ($date ?? new DateTimeImmutable('1970-01-01 00:00:00'))->format(substr($format, 3)) - : '' - ); + return '24:00'.$this->formatSecond($format, $date); } return $this->toDateTime($date)->format($format); @@ -119,4 +116,11 @@ public function __toString(): string { return $this->format(); } + + private function formatSecond(string $format, DateTimeImmutable $date = null): string + { + return strlen($format) > 3 + ? ($date ?? new DateTimeImmutable('1970-01-01 00:00:00'))->format(substr($format, 3)) + : ''; + } } diff --git a/tests/OpeningHoursTest.php b/tests/OpeningHoursTest.php index 9924f62..7694a92 100644 --- a/tests/OpeningHoursTest.php +++ b/tests/OpeningHoursTest.php @@ -905,6 +905,7 @@ public function it_can_set_the_timezone_on_the_openings_hours_object() /** * @test + * * @dataProvider timezones */ public function it_can_handle_timezone_for_date_string($timezone) @@ -1441,4 +1442,34 @@ public function testHoursRangeAreKept() $this->assertNull($monday[0]->getData()); $this->assertSame('09:00-12:00,13:00-18:00', (string) $monday); } + + public function testSearchWithEmptyHours() + { + $openingHours = OpeningHours::create([ + 'monday' => [], + 'tuesday' => [], + 'wednesday' => [], + 'thursday' => [], + 'friday' => [], + 'saturday' => [], + 'sunday' => [], + 'exceptions' => [ + '2016-11-11' => ['09:00-12:00'], + ], + ]); + + $minutes = $openingHours->diffInClosedMinutes( + new DateTimeImmutable('2023-05-17 12:00'), + new DateTimeImmutable('2023-05-23 12:00') + ); + + $this->assertSame(6.0, $minutes / 60 / 24); + + $minutes = $openingHours->diffInOpenMinutes( + new DateTimeImmutable('2023-05-17 12:00'), + new DateTimeImmutable('2023-05-23 12:00') + ); + + $this->assertSame(0.0, $minutes); + } }