date_recur-8.x-2.2/src/Rl/RlHelper.php
src/Rl/RlHelper.php
<?php declare(strict_types=1); namespace Drupal\date_recur\Rl; use Drupal\date_recur\DateRange; use Drupal\date_recur\DateRecurHelperInterface; use Drupal\date_recur\Exception\DateRecurHelperArgumentException; use RRule\RRule; use RRule\RSet; use RRule\RfcParser; /** * Helper for recurring rules implemented with rlanvin/rrule. * * @ingroup RLanvinPhpRrule */ class RlHelper implements DateRecurHelperInterface { /** * The RRULE set. * * @var \RRule\RSet */ protected RSet $set; /** * The time zone used to normalize other date objects. * * @var \DateTimeZone */ protected \DateTimeZone $timeZone; /** * Difference between start date and start date end. * * Calculated value. * * @var \DateInterval */ protected \DateInterval $recurDiff; /** * Constructor for DateRecurHelper. * * @param string $string * The repeat rule. * @param \DateTimeInterface $dtStart * The initial occurrence start date. * @param \DateTimeInterface|null $dtStartEnd * The initial occurrence end date, or NULL to use start date. */ public function __construct( string $string, \DateTimeInterface $dtStart, ?\DateTimeInterface $dtStartEnd = NULL, ) { $dtStartEnd ??= clone $dtStart; $this->recurDiff = $dtStart->diff($dtStartEnd); $this->timeZone = $dtStart->getTimezone(); // Ensure the string is prefixed with RRULE if not multiline. if (!\str_contains($string, "\n") && !\str_starts_with($string, 'RRULE:')) { $string = "RRULE:$string"; } $parts = [ 'RRULE' => [], 'RDATE' => [], 'EXRULE' => [], 'EXDATE' => [], ]; $lines = \explode("\n", $string); foreach ($lines as $n => $line) { $line = \trim($line); if (\str_contains($line, ':') === FALSE) { throw new DateRecurHelperArgumentException(\sprintf('Multiline RRULE must be prefixed with either: RRULE, EXDATE, EXRULE, or RDATE. Missing for line %s', $n + 1)); } [$part, $partValue] = \explode(':', $line, 2); $parts[$part] ?? throw new DateRecurHelperArgumentException("Unsupported line: " . $part); $parts[$part][] = $partValue; } if (($count = \count($parts['RRULE'])) !== 1) { throw new DateRecurHelperArgumentException(\sprintf('One RRULE must be provided. %d provided.', $count)); } $this->set = new RSet(); foreach ($parts as $type => $values) { foreach ($values as $value) { switch ($type) { case 'RRULE': $this->set->addRRule(new RRule($value, $dtStart)); break; case 'RDATE': $dates = RfcParser::parseRDate('RDATE:' . $value); \array_walk($dates, function (\DateTimeInterface $value): void { $this->set->addDate($value); }); break; case 'EXDATE': $dates = RfcParser::parseExDate('EXDATE:' . $value); \array_walk($dates, function (\DateTimeInterface $value): void { $this->set->addExDate($value); }); break; case 'EXRULE': $this->set->addExRule($value); } } } } public static function createInstance(string $string, \DateTimeInterface $dtStart, ?\DateTimeInterface $dtStartEnd = NULL): DateRecurHelperInterface { return new static($string, $dtStart, $dtStartEnd); } /** * {@inheritdoc} */ public function getRules(): array { return \array_map( static function (RRule $rule): RlDateRecurRule { // RL returns all parts, even if no values originally provided. Filter // out the useless parts. $parts = \array_filter($rule->getRule()); return new RlDateRecurRule($parts); }, $this->set->getRRules(), ); } public function isInfinite(): bool { return $this->set->isInfinite(); } public function generateOccurrences(?\DateTimeInterface $rangeStart = NULL, ?\DateTimeInterface $rangeEnd = NULL): \Generator { foreach ($this->set as $occurrenceStart) { /** @var \DateTime $occurrence */ $occurrenceEnd = clone $occurrenceStart; $occurrenceEnd->add($this->recurDiff); if ($rangeStart) { if ($occurrenceStart < $rangeStart && $occurrenceEnd < $rangeStart) { continue; } } if ($rangeEnd) { if ($occurrenceStart > $rangeEnd && $occurrenceEnd > $rangeEnd) { break; } } yield new DateRange($occurrenceStart, $occurrenceEnd); } } /** * {@inheritdoc} */ public function getOccurrences(?\DateTimeInterface $rangeStart = NULL, ?\DateTimeInterface $rangeEnd = NULL, ?int $limit = NULL): array { if ($this->isInfinite() && !isset($rangeEnd) && !isset($limit)) { throw new \InvalidArgumentException('An infinite rule must have a date or count limit.'); } $generator = $this->generateOccurrences($rangeStart, $rangeEnd); if (isset($limit)) { if (!\is_int($limit) || $limit < 0) { // Limit must be a number and more than zero. throw new \InvalidArgumentException('Invalid count limit.'); } // Generate occurrences until the limit is reached. $occurrences = []; foreach ($generator as $value) { if (\count($occurrences) >= $limit) { break; } $occurrences[] = $value; } return $occurrences; } return \iterator_to_array($generator); } /** * {@inheritdoc} */ public function getExcluded(): array { // Implementation normally returns the same time zone as the EXDATE from the // rule string, normalize it here. return \array_map( fn (\DateTime $date): \DateTime => $date->setTimezone($this->timeZone), $this->set->getExDates(), ); } public function current(): DateRange { $occurrenceStart = $this->set->current(); $occurrenceEnd = clone $occurrenceStart; $occurrenceEnd->add($this->recurDiff); return new DateRange($occurrenceStart, $occurrenceEnd); } public function next(): void { $this->set->next(); } public function key(): ?int { return $this->set->key(); } public function valid(): bool { return $this->set->valid(); } public function rewind(): void { $this->set->rewind(); } /** * Get the set. * * @return \RRule\RSet * Returns the set. * * @internal this method is specific to rlanvin/rrule implementation only. */ public function getRlRuleset(): RSet { return $this->set; } }