deploy
This commit is contained in:
214
docs/Umsetzungsanweisung/Old-Nexus/src/App/CronExpression.php
Normal file
214
docs/Umsetzungsanweisung/Old-Nexus/src/App/CronExpression.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App;
|
||||
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
final class CronExpression
|
||||
{
|
||||
/** @var array<int, true> */
|
||||
private array $minutes;
|
||||
/** @var array<int, true> */
|
||||
private array $hours;
|
||||
/** @var array<int, true> */
|
||||
private array $daysOfMonth;
|
||||
/** @var array<int, true> */
|
||||
private array $months;
|
||||
/** @var array<int, true> */
|
||||
private array $daysOfWeek;
|
||||
|
||||
private function __construct(
|
||||
private string $expression,
|
||||
array $minutes,
|
||||
array $hours,
|
||||
array $daysOfMonth,
|
||||
array $months,
|
||||
array $daysOfWeek,
|
||||
private bool $daysOfMonthWildcard,
|
||||
private bool $daysOfWeekWildcard
|
||||
) {
|
||||
$this->minutes = $minutes;
|
||||
$this->hours = $hours;
|
||||
$this->daysOfMonth = $daysOfMonth;
|
||||
$this->months = $months;
|
||||
$this->daysOfWeek = $daysOfWeek;
|
||||
}
|
||||
|
||||
public static function parse(string $expression): self
|
||||
{
|
||||
$normalized = preg_replace('/\s+/', ' ', trim($expression)) ?? '';
|
||||
if ($normalized === '') {
|
||||
throw new \InvalidArgumentException('Cron-Ausdruck fehlt.');
|
||||
}
|
||||
|
||||
$parts = explode(' ', $normalized);
|
||||
if (count($parts) !== 5) {
|
||||
throw new \InvalidArgumentException('Cron-Ausdruck muss aus 5 Feldern bestehen.');
|
||||
}
|
||||
|
||||
return new self(
|
||||
$normalized,
|
||||
self::parseField($parts[0], 0, 59),
|
||||
self::parseField($parts[1], 0, 23),
|
||||
self::parseField($parts[2], 1, 31),
|
||||
self::parseField($parts[3], 1, 12, [
|
||||
'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4,
|
||||
'MAY' => 5, 'JUN' => 6, 'JUL' => 7, 'AUG' => 8,
|
||||
'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12,
|
||||
]),
|
||||
self::parseField($parts[4], 0, 6, [
|
||||
'SUN' => 0, 'MON' => 1, 'TUE' => 2, 'WED' => 3,
|
||||
'THU' => 4, 'FRI' => 5, 'SAT' => 6,
|
||||
'7' => 0,
|
||||
]),
|
||||
trim($parts[2]) === '*',
|
||||
trim($parts[4]) === '*'
|
||||
);
|
||||
}
|
||||
|
||||
public function expression(): string
|
||||
{
|
||||
return $this->expression;
|
||||
}
|
||||
|
||||
public function matches(DateTimeImmutable $utcDateTime, DateTimeZone $timezone): bool
|
||||
{
|
||||
$local = $utcDateTime->setTimezone($timezone);
|
||||
$minute = (int) $local->format('i');
|
||||
$hour = (int) $local->format('G');
|
||||
$dayOfMonth = (int) $local->format('j');
|
||||
$month = (int) $local->format('n');
|
||||
$dayOfWeek = (int) $local->format('w');
|
||||
|
||||
if (!isset($this->minutes[$minute]) || !isset($this->hours[$hour]) || !isset($this->months[$month])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dayOfMonthMatch = isset($this->daysOfMonth[$dayOfMonth]);
|
||||
$dayOfWeekMatch = isset($this->daysOfWeek[$dayOfWeek]);
|
||||
if ($this->daysOfMonthWildcard && $this->daysOfWeekWildcard) {
|
||||
$dayMatches = true;
|
||||
} elseif ($this->daysOfMonthWildcard) {
|
||||
$dayMatches = $dayOfWeekMatch;
|
||||
} elseif ($this->daysOfWeekWildcard) {
|
||||
$dayMatches = $dayOfMonthMatch;
|
||||
} else {
|
||||
$dayMatches = $dayOfMonthMatch || $dayOfWeekMatch;
|
||||
}
|
||||
|
||||
return $dayMatches;
|
||||
}
|
||||
|
||||
public function previousRun(DateTimeImmutable $utcDateTime, DateTimeZone $timezone, int $lookbackMinutes = 527040): ?DateTimeImmutable
|
||||
{
|
||||
$cursor = $this->floorToMinute($utcDateTime);
|
||||
for ($i = 0; $i <= $lookbackMinutes; $i++) {
|
||||
if ($this->matches($cursor, $timezone)) {
|
||||
return $cursor;
|
||||
}
|
||||
$cursor = $cursor->sub(new DateInterval('PT1M'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function nextRun(DateTimeImmutable $utcDateTime, DateTimeZone $timezone, int $lookaheadMinutes = 527040): ?DateTimeImmutable
|
||||
{
|
||||
$cursor = $this->floorToMinute($utcDateTime)->add(new DateInterval('PT1M'));
|
||||
for ($i = 0; $i <= $lookaheadMinutes; $i++) {
|
||||
if ($this->matches($cursor, $timezone)) {
|
||||
return $cursor;
|
||||
}
|
||||
$cursor = $cursor->add(new DateInterval('PT1M'));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return array<int, true> */
|
||||
private static function parseField(string $field, int $min, int $max, array $aliases = []): array
|
||||
{
|
||||
$field = strtoupper(trim($field));
|
||||
if ($field === '*') {
|
||||
$all = [];
|
||||
for ($value = $min; $value <= $max; $value++) {
|
||||
$all[$value] = true;
|
||||
}
|
||||
return $all;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
foreach (explode(',', $field) as $segment) {
|
||||
$segment = strtoupper(trim($segment));
|
||||
if ($segment === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$step = 1;
|
||||
if (str_contains($segment, '/')) {
|
||||
[$segment, $stepPart] = explode('/', $segment, 2);
|
||||
if (!is_numeric($stepPart) || (int) $stepPart <= 0) {
|
||||
throw new \InvalidArgumentException('Ungueltiger Cron-Step in Feld "' . $field . '".');
|
||||
}
|
||||
$step = (int) $stepPart;
|
||||
}
|
||||
|
||||
if ($segment === '*') {
|
||||
$start = $min;
|
||||
$end = $max;
|
||||
} elseif (str_contains($segment, '-')) {
|
||||
[$startPart, $endPart] = explode('-', $segment, 2);
|
||||
$start = self::normalizePart($startPart, $aliases);
|
||||
$end = self::normalizePart($endPart, $aliases);
|
||||
} else {
|
||||
$start = self::normalizePart($segment, $aliases);
|
||||
$end = $start;
|
||||
}
|
||||
|
||||
if ($start < $min || $start > $max || $end < $min || $end > $max || $end < $start) {
|
||||
throw new \InvalidArgumentException('Cron-Feld "' . $field . '" liegt ausserhalb des erlaubten Bereichs.');
|
||||
}
|
||||
|
||||
for ($value = $start; $value <= $end; $value += $step) {
|
||||
$values[$value] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($values === []) {
|
||||
throw new \InvalidArgumentException('Cron-Feld "' . $field . '" ist leer.');
|
||||
}
|
||||
|
||||
ksort($values);
|
||||
return $values;
|
||||
}
|
||||
|
||||
private static function normalizePart(string $part, array $aliases): int
|
||||
{
|
||||
$part = strtoupper(trim($part));
|
||||
if ($part === '') {
|
||||
throw new \InvalidArgumentException('Leerer Cron-Wert.');
|
||||
}
|
||||
|
||||
if (array_key_exists($part, $aliases)) {
|
||||
return (int) $aliases[$part];
|
||||
}
|
||||
|
||||
if (!is_numeric($part)) {
|
||||
throw new \InvalidArgumentException('Ungueltiger Cron-Wert "' . $part . '".');
|
||||
}
|
||||
|
||||
return (int) $part;
|
||||
}
|
||||
|
||||
private function floorToMinute(DateTimeImmutable $utcDateTime): DateTimeImmutable
|
||||
{
|
||||
return $utcDateTime->setTime(
|
||||
(int) $utcDateTime->format('H'),
|
||||
(int) $utcDateTime->format('i'),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user