Skip to content

Commit

Permalink
Merge pull request #216 from spatie/feature/cap-search-to-end-date
Browse files Browse the repository at this point in the history
Cap holidays check to end date when calculating diff
  • Loading branch information
kylekatarnls committed Jun 12, 2023
2 parents a6b937b + 34724bb commit c7dba5d
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 25 deletions.
75 changes: 66 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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'));
Expand Down
13 changes: 13 additions & 0 deletions src/Exceptions/SearchLimitReached.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Spatie\OpeningHours\Exceptions;

use DateTimeInterface;

class SearchLimitReached extends Exception
{
public static function forDate(DateTimeInterface $dateTime): self
{
return new self('Search reached the limit: '.$dateTime->format('Y-m-d H:i:s.u e'));
}
}
5 changes: 3 additions & 2 deletions src/Helpers/DiffTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
66 changes: 57 additions & 9 deletions src/OpeningHours.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,7 +25,7 @@ class OpeningHours

use DataTrait, DateTimeCopier, DiffTrait;

/** @var \Spatie\OpeningHours\Day[] */
/** @var \Spatie\OpeningHours\OpeningHoursForDay[] */
protected $openingHours = [];

/** @var \Spatie\OpeningHours\OpeningHoursForDay[] */
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand All @@ -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));

Expand All @@ -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));
Expand All @@ -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);
Expand All @@ -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));
}

Expand All @@ -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;
Expand Down Expand Up @@ -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));
}

Expand Down
14 changes: 9 additions & 5 deletions src/Time.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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))
: '';
}
}
31 changes: 31 additions & 0 deletions tests/OpeningHoursTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}

0 comments on commit c7dba5d

Please sign in to comment.