Add merged opening hours (#104)

This allows to fetch merged opening hours instead of actually edited
opening hours.
They will be merged per valid time span.
The data structure is a bit different as each time span has a list of
week days, each with its own open and end.

Relates: #10246
This commit is contained in:
Daniel Siepmann 2023-01-05 10:30:23 +01:00 committed by GitHub
parent 6a049366b9
commit 928f42e7fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 639 additions and 92 deletions

View file

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Domain\Model\Frontend;
class MergedOpeningHour
{
/**
* @var MergedOpeningHourWeekDay[]
*/
private $weekDays = [];
/**
* @var \DateTimeImmutable|null
*/
private $from;
/**
* @var \DateTimeImmutable|null
*/
private $through;
public function __construct(
array $weekDays,
?\DateTimeImmutable $from,
?\DateTimeImmutable $through
) {
$this->weekDays = array_values($weekDays);
$this->from = $from;
$this->through = $through;
}
/**
* @return MergedOpeningHourWeekDay[]
*/
public function getWeekDays(): array
{
return $this->weekDays;
}
public function getWeekDaysWithMondayFirstWeekDay(): array
{
return $this->sortWeekDays([
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
'PublicHolidays',
]);
}
public function getFrom(): ?\DateTimeImmutable
{
return $this->from;
}
public function getThrough(): ?\DateTimeImmutable
{
return $this->through;
}
private function sortWeekDays(array $sorting): array
{
$days = [];
$weekDays = array_map(function (MergedOpeningHourWeekDay $weekDay) {
return $weekDay->getDayOfWeek();
}, $this->weekDays);
foreach ($sorting as $weekDay) {
$position = array_search($weekDay, $weekDays);
if ($position === false) {
continue;
}
$days[] = $this->weekDays[$position];
}
return $days;
}
}

View file

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Domain\Model\Frontend;
use WerkraumMedia\ThueCat\Domain\TimingFormat;
class MergedOpeningHourWeekDay
{
/**
* @var string
*/
private $opens;
/**
* @var string
*/
private $closes;
/**
* @var string
*/
private $dayOfWeek;
public function __construct(
string $opens,
string $closes,
string $dayOfWeek
) {
$this->opens = $opens;
$this->closes = $closes;
$this->dayOfWeek = $dayOfWeek;
}
public function getOpens(): string
{
return TimingFormat::format($this->opens);
}
public function getCloses(): string
{
return TimingFormat::format($this->closes);
}
public function getDayOfWeek(): string
{
return $this->dayOfWeek;
}
}

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Domain\Model\Frontend;
class MergedOpeningHours implements \Iterator, \Countable
{
/**
* @var MergedOpeningHour[]
*/
private $openingHours = [];
/**
* @var int
*/
private $position = 0;
/**
* @param MergedOpeningHour[] $openingHours
*/
public function __construct(array $openingHours)
{
$this->openingHours = array_values($openingHours);
}
public function current(): MergedOpeningHour
{
return $this->openingHours[$this->position];
}
public function next(): void
{
++$this->position;
}
public function key(): int
{
return $this->position;
}
public function valid(): bool
{
return isset($this->openingHours[$this->position]);
}
public function rewind(): void
{
$this->position = 0;
}
public function count(): int
{
return count($this->openingHours);
}
}

View file

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Domain\Model\Frontend;
use WerkraumMedia\ThueCat\Domain\TimingFormat;
class OpeningHour
{
/**
@ -91,12 +93,12 @@ class OpeningHour
public function getOpens(): string
{
return $this->formatTiming($this->opens);
return TimingFormat::format($this->opens);
}
public function getCloses(): string
{
return $this->formatTiming($this->closes);
return TimingFormat::format($this->closes);
}
public function getDaysOfWeek(): array
@ -158,35 +160,4 @@ class OpeningHour
return $days;
}
/**
* Returns timing in default format.
*
* @return string
*/
private function formatTiming(string $timing): string
{
$parts = $this->getTimingParts($timing);
if ($parts['hour'] === '' || $parts['minutes'] === '') {
return '';
}
return $parts['hour'] . ':' . $parts['minutes'];
}
/**
* Converts the string representationg of a time HH:MM:SS into an array.
*
* @return string[]
*/
private function getTimingParts(string $string): array
{
$parts = explode(':', $string);
return [
'hour' => $parts[0] ?? '',
'minutes' => $parts[1] ?? '',
'seconds' => $parts[2] ?? '',
];
}
}

View file

@ -75,6 +75,53 @@ class OpeningHours implements TypeInterface, \Iterator, \Countable
return array_values($array);
}
public function getMerged(): MergedOpeningHours
{
$weekDaysPerTimeSpan = [];
foreach ($this as $hour) {
$key = $this->buildKey($hour);
if (isset($weekDaysPerTimeSpan[$key]) === false) {
$weekDaysPerTimeSpan[$key] = [
'from' => $hour->getFrom(),
'through' => $hour->getThrough(),
'weekDays' => [],
];
}
$weekDays = array_map(function (string $dayOfWeek) use ($hour) {
return new MergedOpeningHourWeekDay(
$hour->getOpens(),
$hour->getCloses(),
$dayOfWeek
);
}, $hour->getDaysOfWeek());
$weekDaysPerTimeSpan[$key]['weekDays'] = array_merge($weekDaysPerTimeSpan[$key]['weekDays'], $weekDays);
}
$mergedOpeningHours = array_map(function (array $timeSpan) {
return new MergedOpeningHour(
$timeSpan['weekDays'],
$timeSpan['from'],
$timeSpan['through']
);
}, $weekDaysPerTimeSpan);
return new MergedOpeningHours($mergedOpeningHours);
}
private function buildKey(OpeningHour $hour): string
{
$start = '';
if ($hour->getFrom()) {
$start = $hour->getFrom()->format('d.m.Y');
}
$end = '';
if ($hour->getThrough()) {
$end = $hour->getThrough()->format('d.m.Y');
}
return $start . '-' . $end;
}
public function __toString(): string
{
return $this->serialized;

View file

@ -103,11 +103,27 @@ abstract class Place extends Base
return $this->openingHours;
}
public function getMergedOpeningHours(): ?MergedOpeningHours
{
if ($this->openingHours === null) {
return null;
}
return $this->openingHours->getMerged();
}
public function getSpecialOpeningHours(): ?OpeningHours
{
return $this->specialOpeningHours;
}
public function getMergedSpecialOpeningHours(): ?MergedOpeningHours
{
if ($this->specialOpeningHours === null) {
return null;
}
return $this->specialOpeningHours->getMerged();
}
public function getParkingFacilitiesNearBy(): ObjectStorage
{
return $this->parkingFacilityNearBy;

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Domain;
class TimingFormat
{
/**
* Returns timing in default format.
*
* @return string
*/
public static function format(string $timing): string
{
$parts = self::getTimingParts($timing);
if ($parts['hour'] === '' || $parts['minutes'] === '') {
return '';
}
return $parts['hour'] . ':' . $parts['minutes'];
}
/**
* Converts the string representationg of a time HH:MM:SS into an array.
*
* @return string[]
*/
private static function getTimingParts(string $string): array
{
$parts = explode(':', $string);
return [
'hour' => $parts[0] ?? '',
'minutes' => $parts[1] ?? '',
'seconds' => $parts[2] ?? '',
];
}
}

View file

@ -23,35 +23,15 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Service;
use TYPO3\CMS\Core\Context\Context;
class DateBasedFilter
interface DateBasedFilter
{
/**
* @var Context
*/
private $context;
public function __construct(
Context $context
) {
$this->context = $context;
}
/**
* Filters out all objects where the date is prior the reference date.
*
* The reference date is now.
* The reference date depends on implementation.
*/
public function filterOutPreviousDates(
array $listToFilter,
callable $provideDate
): array {
$referenceDate = $this->context->getPropertyFromAspect('date', 'full');
return array_filter($listToFilter, function($elementToFilter) use ($referenceDate, $provideDate) {
$objectDate = $provideDate($elementToFilter);
return $objectDate === null || $objectDate >= $referenceDate;
});
}
): array;
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Service\DateBasedFilter;
use TYPO3\CMS\Core\Context\Context;
use WerkraumMedia\ThueCat\Service\DateBasedFilter;
class FilterBasedOnTypo3Context implements DateBasedFilter
{
/**
* @var Context
*/
private $context;
public function __construct(
Context $context
) {
$this->context = $context;
}
/**
* Filters out all objects where the date is prior the reference date.
*
* The reference date is now.
*/
public function filterOutPreviousDates(
array $listToFilter,
callable $provideDate
): array {
$referenceDate = $this->context->getPropertyFromAspect('date', 'full', new \DateTimeImmutable());
return array_filter($listToFilter, function($elementToFilter) use ($referenceDate, $provideDate) {
$objectDate = $provideDate($elementToFilter);
return $objectDate === null || $objectDate >= $referenceDate;
});
}
}

View file

@ -17,6 +17,7 @@ services:
public: true
WerkraumMedia\ThueCat\Service\DateBasedFilter:
class: 'WerkraumMedia\ThueCat\Service\DateBasedFilter\FilterBasedOnTypo3Context'
public: true
'cache.thuecat_fetchdata':

View file

@ -54,6 +54,19 @@ Features
``Streetcar``.
These can be used with ``f:translate`` ViewHelper to provide proper none technical labels.
* Configure EXT:scheduler table garbage collection task to clean up import records.
It is now possible to select the tables within the TYPO3 scheduler task to be cleaned up.
* Respect ``schema:givenName`` and ``schema:familyName`` of ``schema:author`` for media.
We only respected ``schema:name`` until now.
* Provide new key ``copyrightAuthor`` holding the actual author for convenience.
It will pick the ``author`` value falling back to ``license.author``.
* Provide new method to retrieve merged opening hours and merged special opening hours.
The merge happens on the valid time span of each.
The data structure is a bit different as different hours will be merged.
Fixes
-----

View file

@ -1,35 +0,0 @@
1.3.1
=====
Breaking
--------
Nothing
Features
--------
* Configure EXT:scheduler table garbage collection task to clean up import records.
It is now possible to select the tables within the TYPO3 scheduler task to be cleaned up.
* Respect ``schema:givenName`` and ``schema:familyName`` of ``schema:author`` for media.
We only respected ``schema:name`` until now.
* Provide new key ``copyrightAuthor`` holding the actual author for convenience.
It will pick the ``author`` value falling back to ``license.author``.
Fixes
-----
Nothing
Tasks
-----
Nothing
Deprecation
-----------
Nothing

View file

@ -23,10 +23,15 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Tests\Unit\Domain\Model\Frontend;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\MergedOpeningHour;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\MergedOpeningHours;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\OpeningHours;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\ParkingFacility;
use WerkraumMedia\ThueCat\Domain\Model\Frontend\TouristAttraction;
use PHPUnit\Framework\TestCase;
use WerkraumMedia\ThueCat\Service\DateBasedFilter;
/**
* @covers \WerkraumMedia\ThueCat\Domain\Model\Frontend\TouristAttraction
@ -100,4 +105,190 @@ class TouristAttractionTest extends TestCase
],
], $subject->getDistanceToPublicTransport());
}
/**
* @test
*/
public function returnsMergedOpeningHours(): void
{
GeneralUtility::addInstance(DateBasedFilter::class, new class implements DateBasedFilter {
public function filterOutPreviousDates(
array $listToFilter,
callable $provideDate
): array {
return $listToFilter;
}
});
$openingHours = new OpeningHours(json_encode([
[
'opens' => '12:00',
'closes' => '14:00',
'daysOfWeek' => [
'Sunday',
],
'from' => [
'date' => '@' . (new \DateTimeImmutable())->format('U'),
],
'through' => [
'date' => '@' . (new \DateTimeImmutable())->modify('+2 days')->format('U'),
],
],
[
'opens' => '10:00',
'closes' => '16:00',
'daysOfWeek' => [
'Monday',
'Tuesday',
],
'from' => [
'date' => '@' . (new \DateTimeImmutable())->format('U'),
],
'through' => [
'date' => '@' . (new \DateTimeImmutable())->modify('+2 days')->format('U'),
],
],
[
'opens' => '13:00',
'closes' => '15:00',
'daysOfWeek' => [
'Saturday',
],
'from' => [
'date' => '@' . (new \DateTimeImmutable())->format('U'),
],
'through' => [
'date' => '@' . (new \DateTimeImmutable())->modify('+3 days')->format('U'),
],
],
]) ?: '');
$subject = new TouristAttraction();
$subject->_setProperty('openingHours', $openingHours);
$result = $subject->getMergedOpeningHours();
self::assertInstanceOf(MergedOpeningHours::class, $result);
self::assertCount(2, $result);
foreach ($result as $index => $mergedHour) {
self::assertInstanceOf(MergedOpeningHour::class, $mergedHour);
$today = (new \DateTimeImmutable())->format('Y-m-d');
$inTwoDays = (new \DateTimeImmutable())->modify('+2 days')->format('Y-m-d');
$inThreeDays = (new \DateTimeImmutable())->modify('+3 days')->format('Y-m-d');
if ($index === 0) {
self::assertSame($today, $mergedHour->getFrom() ? $mergedHour->getFrom()->format('Y-m-d') : '');
self::assertSame($inTwoDays, $mergedHour->getThrough() ? $mergedHour->getThrough()->format('Y-m-d') : '');
self::assertCount(3, $mergedHour->getWeekDays());
self::assertSame('Sunday', $mergedHour->getWeekDays()[0]->getDayOfWeek());
self::assertSame('12:00', $mergedHour->getWeekDays()[0]->getOpens());
self::assertSame('14:00', $mergedHour->getWeekDays()[0]->getCloses());
self::assertSame('Monday', $mergedHour->getWeekDays()[1]->getDayOfWeek());
self::assertSame('10:00', $mergedHour->getWeekDays()[1]->getOpens());
self::assertSame('16:00', $mergedHour->getWeekDays()[1]->getCloses());
self::assertSame('Tuesday', $mergedHour->getWeekDays()[2]->getDayOfWeek());
self::assertSame('10:00', $mergedHour->getWeekDays()[2]->getOpens());
self::assertSame('16:00', $mergedHour->getWeekDays()[2]->getCloses());
} elseif ($index === 1) {
self::assertSame($today, $mergedHour->getFrom() ? $mergedHour->getFrom()->format('Y-m-d') : '');
self::assertSame($inThreeDays, $mergedHour->getThrough() ? $mergedHour->getThrough()->format('Y-m-d') : '');
self::assertCount(1, $mergedHour->getWeekDays());
self::assertSame('Saturday', $mergedHour->getWeekDays()[0]->getDayOfWeek());
self::assertSame('13:00', $mergedHour->getWeekDays()[0]->getOpens());
self::assertSame('15:00', $mergedHour->getWeekDays()[0]->getCloses());
}
}
}
/**
* @test
*/
public function returnsMergedSpecialOpeningHours(): void
{
GeneralUtility::addInstance(DateBasedFilter::class, new class implements DateBasedFilter {
public function filterOutPreviousDates(
array $listToFilter,
callable $provideDate
): array {
return $listToFilter;
}
});
$openingHours = new OpeningHours(json_encode([
[
'opens' => '12:00',
'closes' => '14:00',
'daysOfWeek' => [
'Sunday',
],
'from' => [
'date' => '@' . (new \DateTimeImmutable())->format('U'),
],
'through' => [
'date' => '@' . (new \DateTimeImmutable())->modify('+2 days')->format('U'),
],
],
[
'opens' => '10:00',
'closes' => '16:00',
'daysOfWeek' => [
'Monday',
'Tuesday',
],
'from' => [
'date' => '@' . (new \DateTimeImmutable())->format('U'),
],
'through' => [
'date' => '@' . (new \DateTimeImmutable())->modify('+2 days')->format('U'),
],
],
[
'opens' => '13:00',
'closes' => '15:00',
'daysOfWeek' => [
'Saturday',
],
'from' => [
'date' => '@' . (new \DateTimeImmutable())->format('U'),
],
'through' => [
'date' => '@' . (new \DateTimeImmutable())->modify('+3 days')->format('U'),
],
],
]) ?: '');
$subject = new TouristAttraction();
$subject->_setProperty('specialOpeningHours', $openingHours);
$result = $subject->getMergedSpecialOpeningHours();
self::assertInstanceOf(MergedOpeningHours::class, $result);
self::assertCount(2, $result);
foreach ($result as $index => $mergedHour) {
self::assertInstanceOf(MergedOpeningHour::class, $mergedHour);
$today = (new \DateTimeImmutable())->format('Y-m-d');
$inTwoDays = (new \DateTimeImmutable())->modify('+2 days')->format('Y-m-d');
$inThreeDays = (new \DateTimeImmutable())->modify('+3 days')->format('Y-m-d');
if ($index === 0) {
self::assertSame($today, $mergedHour->getFrom() ? $mergedHour->getFrom()->format('Y-m-d') : '');
self::assertSame($inTwoDays, $mergedHour->getThrough() ? $mergedHour->getThrough()->format('Y-m-d') : '');
self::assertCount(3, $mergedHour->getWeekDaysWithMondayFirstWeekDay());
self::assertSame('Monday', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[0]->getDayOfWeek());
self::assertSame('10:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[0]->getOpens());
self::assertSame('16:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[0]->getCloses());
self::assertSame('Tuesday', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[1]->getDayOfWeek());
self::assertSame('10:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[1]->getOpens());
self::assertSame('16:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[1]->getCloses());
self::assertSame('Sunday', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[2]->getDayOfWeek());
self::assertSame('12:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[2]->getOpens());
self::assertSame('14:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[2]->getCloses());
} elseif ($index === 1) {
self::assertSame($today, $mergedHour->getFrom() ? $mergedHour->getFrom()->format('Y-m-d') : '');
self::assertSame($inThreeDays, $mergedHour->getThrough() ? $mergedHour->getThrough()->format('Y-m-d') : '');
self::assertCount(1, $mergedHour->getWeekDaysWithMondayFirstWeekDay());
self::assertSame('Saturday', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[0]->getDayOfWeek());
self::assertSame('13:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[0]->getOpens());
self::assertSame('15:00', $mergedHour->getWeekDaysWithMondayFirstWeekDay()[0]->getCloses());
}
}
}
}

View file

@ -11,7 +11,7 @@ $EM_CONF[$_EXTKEY] = [
'author' => 'Daniel Siepmann',
'author_email' => 'coding@daniel-siepmann.de',
'author_company' => '',
'version' => '1.3.1',
'version' => '1.3.0',
'constraints' => [
'depends' => [
'core' => '',