Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next run #21

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
/.env
/.idea/
/vendor/
*.result.cache
composer.lock
coverage.xml
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"php": ">=7.0"
},
"require-dev": {
"phpunit/phpunit": "^6.5 || ^7.5"
"phpunit/phpunit": "^6.5 || ^7.5 || ^8.5"
},
"scripts": {
"test": "phpunit",
Expand Down
72 changes: 71 additions & 1 deletion src/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace Ahc\Cron;

use DateTime;

/**
* Cron Expression Parser.
*
Expand All @@ -32,10 +34,14 @@ class Expression
/** @var Normalizer */
protected $normalizer;

/** @var Ticks */
protected $ticks;

public function __construct(SegmentChecker $checker = null, Normalizer $normalizer = null)
{
$this->checker = $checker ?: new SegmentChecker;
$this->normalizer = $normalizer ?: new Normalizer;
$this->ticks = new Ticks($this);

if (null === static::$instance) {
static::$instance = $this;
Expand Down Expand Up @@ -64,6 +70,55 @@ public static function isDue(string $expr, $time = null): bool
return static::instance()->isCronDue($expr, $time);
}

/**
* Next DateTime when the expr would be due again.
*
* @param string $expr The cron expression.
* @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
*
* @throws \RuntimeException
* @throws \UnexpectedValueException
*
* @return \DateTime
*/
public function nextTick(string $expr, $time = null): DateTime
{
return $this->ticks->next($expr, $time);
}

/**
* Next DateTime when the expr would be due again.
*
* @param string $expr The cron expression.
* @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
*
* @throws \RuntimeException
* @throws \UnexpectedValueException
*
* @return \DateTime
*/
public static function next(string $expr, $time = null): DateTime
{
return static::instance()->nextTick($expr, $time);
}

/**
* Next date time as formatted string when the expr would be due again.
*
* @param string $expr The cron expression.
* @param mixed $time The timestamp to validate the cron expr against. Defaults to now.
* @param string $fmt The format
*
* @throws \RuntimeException
* @throws \UnexpectedValueException
*
* @return \DateTime
*/
public static function nextf(string $expr, $time = null, string $fmt = 'Y-m-d H:i:s'): string
{
return static::next($expr, $time)->format($fmt);
}

/**
* Filter only the jobs that are due.
*
Expand All @@ -88,10 +143,15 @@ public static function getDues(array $jobs, $time = null): array
* @return bool
*/
public function isCronDue(string $expr, $time = null): bool
{
return $this->segmentsDue($this->segments($expr), $time);
}

public function segmentsDue(array $segments, $time = null): bool
{
$this->checker->setReference(new ReferenceTime($time));

foreach (\explode(' ', $this->normalizer->normalizeExpr($expr)) as $pos => $segment) {
foreach ($segments as $pos => $segment) {
if ($segment === '*' || $segment === '?') {
continue;
}
Expand All @@ -104,6 +164,16 @@ public function isCronDue(string $expr, $time = null): bool
return true;
}

public function segments(string $expr): array
{
return \explode(' ', $this->normalizer->normalizeExpr($expr));
}

public function segmentChecker(): SegmentChecker
{
return $this->checker;
}

/**
* Filter only the jobs that are due.
*
Expand Down
46 changes: 42 additions & 4 deletions src/ReferenceTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace Ahc\Cron;

use DateTime;

/**
* @method int minute()
* @method int hour()
Expand Down Expand Up @@ -45,14 +47,50 @@ class ReferenceTime
/** @var array The Magic methods */
protected $methods = [];

public $timestamp;

public function __construct($time)
{
$timestamp = $this->normalizeTime($time);

$this->values = $this->parse($timestamp);
$this->reset($this->normalizeTime($time));
$this->methods = (new \ReflectionClass($this))->getConstants();
}

public function reset(int $timestamp)
{
$this->timestamp = $timestamp;
$this->values = $this->parse($timestamp);
}

public function add(int $sec)
{
$this->reset($this->timestamp + $sec);
}

public function addMonth()
{
$year = $this->values[self::YEAR];
$month = $this->values[self::MONTH] + 1;

if ($month > 12) {
[$year, $month] = [$year + 1, 1];
}

$new = "$year-$month-" . date('d H:i:s', $this->timestamp);
$this->reset(\strtotime($new));
}

public function addYear()
{
$year = $this->values[self::YEAR] + 1;
$new = "$year-" . date('m-d H:i:s', $this->timestamp);
$this->reset(\strtotime($new));
}

public function dateTime(): DateTime
{
return new DateTime(date('Y-m-d H:i:s', $this->timestamp));
}

public function __call(string $method, array $args): int
{
$method = \preg_replace('/^GET/', '', \strtoupper($method));
Expand Down Expand Up @@ -81,7 +119,7 @@ protected function normalizeTime($time): int
$time = \time();
} elseif (\is_string($time)) {
$time = \strtotime($time);
} elseif ($time instanceof \DateTime) {
} elseif ($time instanceof DateTime) {
$time = $time->getTimestamp();
}

Expand Down
6 changes: 4 additions & 2 deletions src/SegmentChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ public function __construct(Validator $validator = null)
$this->validator = $validator ?: new Validator;
}

public function setReference(ReferenceTime $reference)
public function setReference(ReferenceTime $reference): self
{
$this->reference = $reference;

return $this;
}

/**
Expand Down Expand Up @@ -96,7 +98,7 @@ protected function checkModifier(string $offset, int $pos): bool
return $this->validator->isValidWeekDay($offset, $this->reference);
}

$this->validator->unexpectedValue($pos, $offset);
throw $this->validator->unexpectedValue($pos, $offset);
// @codeCoverageIgnoreStart
}
// @codeCoverageIgnoreEnd
Expand Down
97 changes: 97 additions & 0 deletions src/Ticks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

/*
* This file is part of the PHP-CRON-EXPR package.
*
* (c) Jitendra Adhikari <[email protected]>
* <https://github.com/adhocore>
*
* Licensed under MIT license.
*/

namespace Ahc\Cron;

use DateTime;

/**
* Ticks for next/prev ticks of an expr based on given time.
* Next tick support is experimental.
* Prev tick is not yet implemented.
*/
class Ticks
{
/** @var Expression */
protected $expr;

protected $limits = [
ReferenceTime::MINUTE => 60,
ReferenceTime::HOUR => 24,
ReferenceTime::MONTHDAY => 31,
ReferenceTime::MONTH => 12,
ReferenceTime::WEEKDAY => 366,
ReferenceTime::YEAR => 100,
];

public function __construct(Expression $expr)
{
$this->expr = $expr;
}

public function next(string $expr, $time = null): DateTime
{
$checker = $this->expr->segmentChecker();
$segments = $this->expr->segments($expr);

$iter = 500;
$ref = new ReferenceTime($time);
$ref->add(60 - $ref->timestamp % 60); // truncate seconds

over:
while ($iter > 0) {
$iter--;
foreach ($segments as $pos => $seg) {
if ($seg === '*' || $seg === '?') {
continue;
}
[$new, $isOk] = $this->bumpUntilDue($checker, $seg, $pos, $ref);
if ($isOk) {
$ref = $new;
goto over;
}
}
}

$date = $ref->dateTime();
if ($this->expr->segmentsDue($segments, $date)) {
return $date;
}

throw new \RuntimeException('Tried so hard');
}

private function bumpUntilDue(SegmentChecker $checker, string $seg, int $pos, ReferenceTime $ref): array
{
$iter = $this->limits[$pos];
while ($iter > 0) {
$iter--;
if ($checker->setReference($ref)->checkDue($seg, $pos)) {
return [$ref, true];
}
if ($pos === ReferenceTime::MINUTE) {
$ref->add(60);
} elseif ($pos === ReferenceTime::HOUR) {
$ref->add(3600);
} elseif ($pos === ReferenceTime::MONTHDAY || $pos === ReferenceTime::WEEKDAY) {
$ref->add(86400);
} elseif ($pos === ReferenceTime::MONTH) {
$ref->addMonth();
} elseif ($pos === ReferenceTime::YEAR) {
$ref->addYear();
}
}

return [$ref, false];
}
}
12 changes: 7 additions & 5 deletions src/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace Ahc\Cron;

use UnexpectedValueException;

/**
* Cron segment validator.
*
Expand Down Expand Up @@ -108,7 +110,7 @@ public function isValidMonthDay(string $value, ReferenceTime $reference): bool
return $this->isClosestWeekDay((int) $value, $month, $reference);
}

$this->unexpectedValue(2, $value);
throw $this->unexpectedValue(2, $value);
// @codeCoverageIgnoreStart
}

Expand Down Expand Up @@ -153,7 +155,7 @@ public function isValidWeekDay(string $value, ReferenceTime $reference): bool
}

if (!\strpos($value, '#')) {
$this->unexpectedValue(4, $value);
throw $this->unexpectedValue(4, $value);
}

list($day, $nth) = \explode('#', \str_replace('7#', '0#', $value));
Expand All @@ -171,11 +173,11 @@ public function isValidWeekDay(string $value, ReferenceTime $reference): bool
* @param int $pos
* @param string $value
*
* @throws \UnexpectedValueException
* @return \UnexpectedValueException
*/
public function unexpectedValue(int $pos, string $value)
public function unexpectedValue(int $pos, string $value): UnexpectedValueException
{
throw new \UnexpectedValueException(
return new UnexpectedValueException(
\sprintf('Invalid offset value at segment #%d: %s', $pos, $value)
);
}
Expand Down
Loading