Skip to content

Next run #21

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
@@ -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
@@ -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",
72 changes: 71 additions & 1 deletion src/Expression.php
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@

namespace Ahc\Cron;

use DateTime;

/**
* Cron Expression Parser.
*
@@ -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;
@@ -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.
*
@@ -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;
}
@@ -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.
*
46 changes: 42 additions & 4 deletions src/ReferenceTime.php
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@

namespace Ahc\Cron;

use DateTime;

/**
* @method int minute()
* @method int hour()
@@ -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));
@@ -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();
}

6 changes: 4 additions & 2 deletions src/SegmentChecker.php
Original file line number Diff line number Diff line change
@@ -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;
}

/**
@@ -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
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 <jiten.adhikary@gmail.com>
* <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
@@ -13,6 +13,8 @@

namespace Ahc\Cron;

use UnexpectedValueException;

/**
* Cron segment validator.
*
@@ -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
}

@@ -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));
@@ -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)
);
}
Loading