mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-17 07:44:24 +00:00
192 lines
5.3 KiB
PHP
192 lines
5.3 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Chill is a software for social workers
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE file that was distributed with this source code.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Chill\MainBundle\Util;
|
|
|
|
use DateTimeImmutable;
|
|
use DateTimeInterface;
|
|
use DateTimeZone;
|
|
use LogicException;
|
|
|
|
use function array_diff;
|
|
use function array_flip;
|
|
use function array_intersect_key;
|
|
use function array_key_exists;
|
|
use function array_merge;
|
|
use function array_unique;
|
|
use function array_values;
|
|
use function count;
|
|
use function ksort;
|
|
|
|
use const PHP_INT_MAX;
|
|
|
|
/**
|
|
* Utilities to compare date periods.
|
|
*
|
|
* This class allow to compare periods when there are period covering. The
|
|
* argument `minCovers` allow to find also when there are more than 2 period
|
|
* which intersects.
|
|
*
|
|
* Example: a team may have maximum 2 leaders on a same period: you will
|
|
* find here all periods where there are more than 2 leaders.
|
|
*
|
|
* Usage:
|
|
*
|
|
* ```php
|
|
* $cover = new DateRangeCovering(2); // 2 means we will have periods
|
|
* // when there are 2+ periods intersecting
|
|
* $cover
|
|
* ->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), null)
|
|
* ->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), null)
|
|
* ->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), null)
|
|
* ->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), null)
|
|
* ->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), null)
|
|
* ->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), null)
|
|
* ->compute()
|
|
* ;
|
|
* $cover->getIntersections();
|
|
* ```
|
|
*/
|
|
class DateRangeCovering
|
|
{
|
|
private bool $computed = false;
|
|
|
|
private array $intersections = [];
|
|
|
|
private array $intervals = [];
|
|
|
|
private array $metadatas = [];
|
|
|
|
private int $minCover;
|
|
|
|
private array $sequence = [];
|
|
|
|
private DateTimeZone $tz;
|
|
|
|
private int $uniqueKeyCounter = 0;
|
|
|
|
/**
|
|
* @param int $minCover the minimum of covering required
|
|
*/
|
|
public function __construct(int $minCover, DateTimeZone $tz)
|
|
{
|
|
if (0 > $minCover) {
|
|
throw new LogicException('argument minCover cannot be lower than 0');
|
|
}
|
|
|
|
$this->minCover = $minCover;
|
|
$this->tz = $tz;
|
|
}
|
|
|
|
public function add(DateTimeInterface $start, ?DateTimeInterface $end = null, $metadata = null): self
|
|
{
|
|
if ($this->computed) {
|
|
throw new LogicException('You cannot add intervals to a computed instance');
|
|
}
|
|
|
|
$k = $this->uniqueKeyCounter++;
|
|
$this->intervals[$k] = [$start, $end];
|
|
$this->metadatas[$k] = $metadata;
|
|
|
|
$this->addToSequence($start->getTimestamp(), $k, null);
|
|
$this->addToSequence(
|
|
null === $end ? PHP_INT_MAX : $end->getTimestamp(),
|
|
null,
|
|
$k
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function compute(): self
|
|
{
|
|
ksort($this->sequence);
|
|
|
|
$currentPeriod = [];
|
|
$currents = [];
|
|
$isOpen = false;
|
|
$overs = [];
|
|
|
|
foreach ($this->sequence as $ts => $moves) {
|
|
$currents = array_merge($currents, $moves['s']);
|
|
$currents = array_diff($currents, $moves['e']);
|
|
|
|
if (count($currents) > $this->minCover && !$isOpen) {
|
|
$currentPeriod[0] = $ts;
|
|
$currentPeriod[2] = $currents;
|
|
$isOpen = true;
|
|
} elseif ($isOpen && count($currents) <= $this->minCover) {
|
|
$currentPeriod[1] = $ts;
|
|
$overs[] = $currentPeriod;
|
|
$currentPeriod = [];
|
|
$isOpen = false;
|
|
} elseif ($isOpen) {
|
|
$currentPeriod[2] = array_merge($currentPeriod[2], $currents);
|
|
}
|
|
}
|
|
|
|
// process metadata
|
|
foreach ($overs as [$start, $end, $metadata]) {
|
|
$this->intersections[] = [
|
|
(new DateTimeImmutable('@' . $start))
|
|
->setTimezone($this->tz),
|
|
PHP_INT_MAX === $end ? null : (new DateTimeImmutable('@' . $end))
|
|
->setTimezone($this->tz),
|
|
array_values(
|
|
array_intersect_key(
|
|
$this->metadatas,
|
|
array_flip(array_unique($metadata))
|
|
)
|
|
),
|
|
];
|
|
}
|
|
|
|
$this->computed = true;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getIntersections(): array
|
|
{
|
|
if (!$this->computed) {
|
|
throw new LogicException(sprintf('You cannot call the method %s before ' .
|
|
"'process'", __METHOD__));
|
|
}
|
|
|
|
return $this->intersections;
|
|
}
|
|
|
|
public function hasIntersections(): bool
|
|
{
|
|
if (!$this->computed) {
|
|
throw new LogicException(sprintf('You cannot call the method %s before ' .
|
|
"'process'", __METHOD__));
|
|
}
|
|
|
|
return count($this->intersections) > 0;
|
|
}
|
|
|
|
private function addToSequence($timestamp, ?int $start = null, ?int $end = null)
|
|
{
|
|
if (!array_key_exists($timestamp, $this->sequence)) {
|
|
$this->sequence[$timestamp] = ['s' => [], 'e' => []];
|
|
}
|
|
|
|
if (null !== $start) {
|
|
$this->sequence[$timestamp]['s'][] = $start;
|
|
}
|
|
|
|
if (null !== $end) {
|
|
$this->sequence[$timestamp]['e'][] = $end;
|
|
}
|
|
}
|
|
}
|