Check if current timestamp matches crontab time pattern in PHP [duplicate] - php

I need to develop a task system that should be able to work on servers that doesn't support crontab.
I'm asking if there is any existing code that can take a cron string (e.g. '0 0,12 1 */2 *' and return the timestamp of the next scheduled run.
If such a code couldn't be found then how should I start with that?

You could use this class PHP-Parse-cron-strings-and-compute-schedules
It'll also compute the last scheduled run

Use this function:
function parse_crontab($time, $crontab)
{$time=explode(' ', date('i G j n w', strtotime($time)));
$crontab=explode(' ', $crontab);
foreach ($crontab as $k=>&$v)
{$time[$k]=intval($time[$k]);
$v=explode(',', $v);
foreach ($v as &$v1)
{$v1=preg_replace(array('/^\*$/', '/^\d+$/', '/^(\d+)\-(\d+)$/', '/^\*\/(\d+)$/'),
array('true', $time[$k].'===\0', '(\1<='.$time[$k].' and '.$time[$k].'<=\2)', $time[$k].'%\1===0'),
$v1
);
}
$v='('.implode(' or ', $v).')';
}
$crontab=implode(' and ', $crontab);
return eval('return '.$crontab.';');
}
var_export(parse_crontab('2011-05-04 02:08:03', '*/2,3-5,9 2 3-5 */2 *'));
var_export(parse_crontab('2011-05-04 02:08:03', '*/8 */2 */4 */5 *'));

You can try this: http://mtdowling.com/blog/2012/06/03/cron-expressions-in-php/ which use PHP Cron-Expression parser library, a php class https://github.com/mtdowling/cron-expression

I found diyism had a great answer, but found a crucial bug.
If you enter a cron time such as 0 * * * *, it'll run at 0 minute, 8th, minute and 9th minute. The code gives a conditional 08===0, which returns true, because PHP interprets numbers starting with 0 as octal, and 08 and 09 are not valid octal numbers so they're interpreted as 0. More information here.
How to prevent PHP from doing octal math in conditionals? (why does 08 === 0)
Here's a fixed and well commented version of diyism's code.
// Parse CRON frequency
function parse_crontab($time, $crontab) {
// Get current minute, hour, day, month, weekday
$time = explode(' ', date('i G j n w', strtotime($time)));
// Split crontab by space
$crontab = explode(' ', $crontab);
// Foreach part of crontab
foreach ($crontab as $k => &$v) {
// Remove leading zeros to prevent octal comparison, but not if number is already 1 digit
$time[$k] = preg_replace('/^0+(?=\d)/', '', $time[$k]);
// 5,10,15 each treated as seperate parts
$v = explode(',', $v);
// Foreach part we now have
foreach ($v as &$v1) {
// Do preg_replace with regular expression to create evaluations from crontab
$v1 = preg_replace(
// Regex
array(
// *
'/^\*$/',
// 5
'/^\d+$/',
// 5-10
'/^(\d+)\-(\d+)$/',
// */5
'/^\*\/(\d+)$/'
),
// Evaluations
// trim leading 0 to prevent octal comparison
array(
// * is always true
'true',
// Check if it is currently that time,
$time[$k] . '===\0',
// Find if more than or equal lowest and lower or equal than highest
'(\1<=' . $time[$k] . ' and ' . $time[$k] . '<=\2)',
// Use modulus to find if true
$time[$k] . '%\1===0'
),
// Subject we are working with
$v1
);
}
// Join 5,10,15 with `or` conditional
$v = '(' . implode(' or ', $v) . ')';
}
// Require each part is true with `and` conditional
$crontab = implode(' and ', $crontab);
// Evaluate total condition to find if true
return eval('return ' . $crontab . ';');
}

I wrote a very powerful PHP class called CalendarEvent a long time ago:
https://github.com/cubiclesoft/php-misc/
It supports two different pattern syntaxes. Normal cron cannot handle certain complex patterns whereas the default CalendarEvent syntax handles any scheduling pattern you could ever need. The cron pattern syntax support is actually a fallback (prefix cron lines with cron and a space).
CalendarEvent was written mostly as a calendar event calculation class and just happened to support cron style "next trigger" mechanisms. It is actually designed to calculate an entire schedule for multiple months for the purpose of displaying a calendar to a user (hence the class name). I've also used the class as an intermediate solution to translate events between calendar platforms. That's the rarer scenario - I've more frequently utilized it for AddSchedule()/NextTrigger() cron-like solutions.

You can use the popular package PHP Cron Expression Parser: https://github.com/dragonmantank/cron-expression
This also is the built-in part of the Laravel Framework)

In the parse_crontab function:
Replace $time[$k] with intval($time[$k]) inside the preg_replace line
to compare two base10 numbers correctly.

Related

Make an if-statement understand which "time-left" format is bigger

I am trying to make an IF-statement that can calculate which "time-left" is bigger.
This is my code.
if('6h : 27m : 45s' > '6h : 27m : 15s') {
echo 'Timer to the left is bigger';
}
This code works fine, it actually prints Timer to the left is bigger.
However, when any time value (hour, minute or seconds) is only one digit instead of two. It doesn't work. Like this:
if('6h : 27m : 45s' > '6h : 27m : 5s') {
echo 'Timer to the left is bigger';
}
This time it does not print out Timer to the left is bigger. It is because the timer to the right is only 5s left, which is less than 15s, but it is because it is only one digit instead of two, and the IF-statement doesn't understand that.
How can I parse this to an understandable format for the if-statement?
NOTE the string is from an API, I can't edit it in anyway.
php function version_compare() is like strcmp(), but compares numbers as single unit. It has some side effects because of (virtually) converting chars, but I think it does not matter here:
So
if('6h : 27m : 45s' > '6h : 27m : 15s')
becomes
if( version_compare('6h : 27m : 45s','6h : 27m : 15s') > 0 )
Your post suggests that all three time units are always available. So just create two arrays of numbers how ever you wish. PHP7's spaceship operator will make numeric comparisons when handling two numeric values. It will compare the elements from left to right as you require.
No time parsing is necessary because the time units are ordered from greater to lesser.
Code: (Demo)
$left = '6h : 27m : 5s';
$right = '6h : 27m : 15s';
$lookup = [
-1 => 'less',
0 => 'equal',
1 => 'more'
];
$lArray = preg_split('/\D+/', $left, 0, PREG_SPLIT_NO_EMPTY);
$rArray = preg_split('/\D+/', $right, 0, PREG_SPLIT_NO_EMPTY);
$comparison = $lArray <=> $rArray;
echo $comparison . ' -> ' . $lookup[$comparison];
Output:
-1 -> less
You make write a conditional check for -1 if that is all that you require. The lookup was purely to clarify the demonstration.
In other words:
if ((preg_split('/\D+/', $left, 0, PREG_SPLIT_NO_EMPTY) <=> preg_split('/\D+/', $right, 0, PREG_SPLIT_NO_EMPTY)) == -1) {
// left is less than right
}
Or use a non-regex alternative that will cast each matched number as an integer then compare the two generated arrays containing three integers each. (Demo)
$format = '%dh : %dm %ds';
$comparison = sscanf($left, $format) <=> sscanf($right, $format);
I just noticed Wiimm's outside-the-box call of version_compare() to parse your time expressions. (+1 from me, but I think I need to explain more about it) While not its intended use, it is a clever and reliable non-regex technique because your time unit substrings (h, m, s) are not found in the list of development-stage substrings which hold special meaning uniformly positioned in both strings. This means that even if you had non-numeric substrings with special meaning, the comparison would still be evaluated correctly based solely on the numeric values.
Special substrings in order:
dev
alpha or a
beta or b
RC or rc
#
pl or p
This function is a three-way-comparing tool - like the spaceship operator. It also accepts a third parameter to command that it returns a boolean response. Using gt will return true if the first string is "greater than" the second string. While the third parameter is an abbreviation, I find it more intuitive than checking for -1.
Code: (Demo)
if (version_compare($left, $right, 'gt')) {
echo 'left is greater than right';
} else {
echo 'left is less than or equal to right';
}
Output:
left is less than or equal to right
It's not the most prettiest way of doing it, but since you get the data returned from an API you can't modify, you'll have to work with the data you got.
There are several ways of doing it. One way is to convert the time into seconds, and compare that. Another, is to convert them into objects - and work with that. The "gruntwork" is the same; you have to split up the data into hours, minutes and seconds, and use that - which is more usable than the format Xh : Ym : Zs.
Here we split up each time into pieces of three, by the delimiter :. This gets us the individual parts - but we still need to fetch the integer value, which is what we're interested in. We can grab that by using inval(), as the number comes first. Once we have the three pieces, we can either put them into a HH:mm:ss format, or you can multiply them up to everything being in seconds, and add them together (first is hours, second is minutes, third is seconds). Here we're not doing that, we glue them together in a readable format and use strtotime() to convert them into a timestamp - which we finally, after all this work, can compare!
$time_left = '6h : 27m : 45s';
$time_right = '6h : 27m : 5s';
$parts_left = explode(":", $time_left);
$parts_right = explode(":", $time_right);
$left = intval($parts_left[0]).":".intval($parts_left[1]).":".intval($parts_left[2]);
$right = intval($parts_right[0]).":".intval($parts_right[1]).":".intval($parts_right[2]);
if (strtotime($left) > strtotime($right)) {
echo 'Timer to the left is bigger';
}
Live demo at https://3v4l.org/uUQVh
According to your edited comment, the hours can be greater than 24 - in which case you need to convert the hours and minutes to seconds, and sum that up.
$time_left = '6h : 27m : 45s';
$time_right = '6h : 27m : 5s';
$parts_left = explode(":", $time_left);
$parts_right = explode(":", $time_right);
$left = intval($parts_left[0]) * 60*60 + intval($parts_left[1])*60 + intval($parts_left[2]);
$right = intval($parts_right[0]) * 60*60 + intval($parts_right[1])*60 + intval($parts_right[2]);
if ($left > $right) {
echo "Timer to the left is bigger";
}
Live demo at https://3v4l.org/DaINX

Scheduling multiple emails PHP

I've made a system that receives a query, executes it at the database and then creates an HTML table with the data returned. These tables are then saved in the system for further access, and the user can send it via email to multiple receivers.
All works well, but now I have to schedule the emails. Need to send "table X" at a week from now and "table Y" at two weeks. How can I do that? I've looked up for CRONS/WindowsTasks but I don't know how I would make it automatic for every table, since the user can keep creating different tables.
I've used CodeIgniter and PHPMailerMaster to make it happen.
Here is a screenshot of the TableViewer, it's in portuguese, but it contains:
|TITLE | CONFIG | SEND | XLS GENERATOR | LAST SENT |
For each table created. (Just so you can understand how it works)
If anyone got any ideas.
Here is a function that simulates crontab in PHP. You can use this for dynamic crontabs, so each table can have it's own unique frequency.
function parse_crontab($time, $crontab) {
// Get current minute, hour, day, month, weekday
$time = explode(' ', date('i G j n w', strtotime($time)));
// Split crontab by space
$crontab = explode(' ', $crontab);
// Foreach part of crontab
foreach ($crontab as $k => &$v) {
// Remove leading zeros to prevent octal comparison, but not if number is already 1 digit
$time[$k] = preg_replace('/^0+(?=\d)/', '', $time[$k]);
// 5,10,15 each treated as seperate parts
$v = explode(',', $v);
// Foreach part we now have
foreach ($v as &$v1) {
// Do preg_replace with regular expression to create evaluations from crontab
$v1 = preg_replace(
// Regex
array(
// *
'/^\*$/',
// 5
'/^\d+$/',
// 5-10
'/^(\d+)\-(\d+)$/',
// */5
'/^\*\/(\d+)$/'
),
// Evaluations
// trim leading 0 to prevent octal comparison
array(
// * is always true
'true',
// Check if it is currently that time,
$time[$k] . '===\0',
// Find if more than or equal lowest and lower or equal than highest
'(\1<=' . $time[$k] . ' and ' . $time[$k] . '<=\2)',
// Use modulus to find if true
$time[$k] . '%\1===0'
),
// Subject we are working with
$v1
);
}
// Join 5,10,15 with `or` conditional
$v = '(' . implode(' or ', $v) . ')';
}
// Require each part is true with `and` conditional
$crontab = implode(' and ', $crontab);
// Evaluate total condition to find if true
return eval('return ' . $crontab . ';');
}
It's used like this. First parameter is the current time, or time you want to check, second parameter is the crontab.
$time_to_run = parse_crontab(date('Y-m-d H:i:s'), '* * * * *')

PHP's array_slice vs Python's splitting arrays

Some background
I was having a go at the common "MaxProfit" programming challenge. It basically goes like this:
Given a zero-indexed array A consisting of N integers containing daily
prices of a stock share for a period of N consecutive days, returns
the maximum possible profit from one transaction during this period.
I was quite pleased with this PHP algorithm I came up, having avoided the naive brute-force attempt:
public function maxProfit($prices)
{
$maxProfit = 0;
$key = 0;
$n = count($prices);
while ($key < $n - 1) {
$buyPrice = $prices[$key];
$maxFuturePrice = max( array_slice($prices, $key+1) );
$profit = $maxFuturePrice - $buyPrice;
if ($profit > $maxProfit) $maxProfit = $profit;
$key++;
}
return $maxProfit;
}
However, having tested my solution it seems to perform badly performance-wise, perhaps even in O(n2) time.
I did a bit of reading around the subject and discovered a very similar python solution. Python has some quite handy array abilities which allow splitting an array with a a[s : e] syntax, unlike in PHP where I used the array_slice function. I decided this must be the bottleneck so I did some tests:
Tests
PHP array_slice()
$n = 10000;
$a = range(0,$n);
$start = microtime(1);
foreach ($a as $key => $elem) {
$subArray = array_slice($a, $key);
}
$end = microtime(1);
echo sprintf("Time taken: %sms", round(1000 * ($end - $start), 4)) . PHP_EOL;
Results:
$ php phpSlice.php
Time taken: 4473.9199ms
Time taken: 4474.633ms
Time taken: 4499.434ms
Python a[s : e]
import time
n = 10000
a = range(0, n)
start = time.time()
for key, elem in enumerate(a):
subArray = a[key : ]
end = time.time()
print "Time taken: {0}ms".format(round(1000 * (end - start), 4))
Results:
$ python pySlice.py
Time taken: 213.202ms
Time taken: 212.198ms
Time taken: 215.7381ms
Time taken: 213.8121ms
Question
Why is PHP's array_slice() around 20x less efficient than Python?
Is there an equivalently efficient method in PHP that achieves the above and thus hopefully makes my maxProfit algorithm run in O(N) time? Edit I realise my implementation above is not actually O(N), but my question still stands regarding the efficiency of slicing arrays.
I don't really know, but PHP's arrays are messed up hybrid monsters, maybe that's why. Python's lists are really just lists, not at the same time dictionaries, so they might be simpler/faster because of that.
Yes, do an actual O(n) solution. Your solution isn't just slow because PHP's slicing is apparently slow, it's slow because you obviously have an O(n^2) algorithm. Just walk over the array once, keep track of the minimum price found so far, and check it with the current price. Not something like max over half the array in every single loop iteration.

Check whether day is specified in a date string

Test case scenario - User clicks on one of two links: 2012/10, or 2012/10/15.
I need to know whether the DAY is specified within the link. I am already stripping the rest of the link (except above) out of my URL, am I am passing the value to an AJAX request to change days on an archive page.
I can do this in either JS or PHP - is checking against the regex /\d{4}\/\d{2}\/\d{2}/ the only approach to seeing if the day was specified or not?
You can also do this if you always get this format: 2012/10 or 2012/10/15
if( str.split("/").length == 3 ) { }
But than there is no guaranty it will be numbers. If you want to be sure they are numbers you do need that kind of regex to match the String.
You could explode the date by the "/" delimiter, then count the items:
$str = "2012/10";
$str2 = "2012/10/5";
echo count(explode("/", $str)); // 2
echo count(explode("/", $str2)); // 3
Or, turn it into a function:
<?php
function getDateParts($date) {
$date = explode("/", $date);
$y = !empty($date[0]) ? $date[0] : date("Y");
$m = !empty($date[1]) ? $date[1] : date("m");
$d = !empty($date[2]) ? $date[2] : date("d");
return array($y, $m, $d);
}
?>
I would personally use a regex, it is a great way of testing this sort of thing. Alternatively, you can split/implode the string on /, you will have an array of 3 strings (hopefully) which you can then test. I'd probably use that technique if I was going to do work with it later.
The easiest and fastest way is to check the length of the string!
In fact, you need to distinguish between: yyyy/mm/dd (which is 10 characters long) and yyyy/mm (which is 7 characters).
if(strlen($str) > 7) {
// Contains day
}
else {
// Does not contain day
}
This will work EVEN if you do not use leading zeros!
In fact:
2013/7/6 -> 8 characters (> 7 -> success)
2013/7 -> 6 characters (< 7 -> success)
This is certainly the fastest code too, as it does not require PHP to iterate over the whole string (as using explode() does).

Whats the cleanest way to convert a 5-7 digit number into xxx/xxx/xxx format in php?

I have sets of 5, 6 and 7 digit numbers. I need them to be displayed in the 000/000/000 format. So for example:
12345 would be displayed as 000/012/345
and
9876543 would be displayed as 009/876/543
I know how to do this in a messy way, involving a series of if/else statements, and strlen functions, but there has to be a cleaner way involving regex that Im not seeing.
sprintf and modulo is one option
function formatMyNumber($num)
{
return sprintf('%03d/%03d/%03d',
$num / 1000000,
($num / 1000) % 1000,
$num % 1000);
}
$padded = str_pad($number, 9, '0', STR_PAD_LEFT);
$split = str_split($padded, 3);
$formatted = implode('/', $split);
You asked for a regex solution, and I love playing with them, so here is a regex solution!
I show it for educational (and fun) purpose only, just use Adam's solution, clean, readable and fast.
function FormatWithSlashes($number)
{
return substr(preg_replace('/(\d{3})?(\d{3})?(\d{3})$/', '$1/$2/$3',
'0000' . $number),
-11, 11);
}
$numbers = Array(12345, 345678, 9876543);
foreach ($numbers as $val)
{
$r = FormatWithSlashes($val);
echo "<p>$r</p>";
}
OK, people are throwing stuff out, so I will too!
number_format would be great, because it accepts a thousands separator, but it doesn't do padding zeroes like sprintf and the like. So here's what I came up with for a one-liner:
function fmt($x) {
return substr(number_format($x+1000000000, 0, ".", "/"), 2);
}
Minor improvement to PhiLho's suggestion:
You can avoid the substr by changing the regex to:
function FormatWithSlashes($number)
{
return preg_replace('/^0*(\d{3})(\d{3})(\d{3})$/', '$1/$2/$3',
'0000' . $number);
}
I also removed the ? after each of the first two capture groups because, when given a 5, 6, or 7 digit number (as specified in the question), this will always have at least 9 digits to work with. If you want to guard against the possibility of receiving a smaller input number, run the regex against '000000000' . $number instead.
Alternately, you could use
substr('0000' . $number, -9, 9);
and then splice the slashes in at the appropriate places with substr_replace, which I suspect may be the fastest way to do this (no need to run regexes or do division), but that's really just getting into pointless optimization, as any of the solutions presented will still be much faster than establishing a network connection to the server.
This would be how I would write it if using Perl 5.10 .
use 5.010;
sub myformat(_;$){
# prepend with zeros
my $_ = 0 x ( 9-length($_[0]) ) . $_[0];
my $join = $_[1] // '/'; # using the 'defined or' operator `//`
# m// in a list context returns ($1,$2,$3,...)
join $join, m/ ^ (\d{3}) (\d{3}) (\d{3}) $ /x;
}
Tested with:
$_ = 11111;
say myformat;
say myformat(2222);
say myformat(33333,';');
say $_;
returns:
000/011/111
000/002/222
000;033;333
11111
Back-ported to Perl 5.8 :
sub myformat(;$$){
local $_ = #_ ? $_[0] : $_
# prepend with zeros
$_ = 0 x ( 9-length($_) ) . $_;
my $join = defined($_[1]) ? $_[1] :'/';
# m// in a list context returns ($1,$2,$3,...)
join $join, m/ ^ (\d{3}) (\d{3}) (\d{3}) $ /x;
}
Here's how I'd do it in python (sorry I don't know PHP as well). I'm sure you can convert it.
def convert(num): #num is an integer
a = str(num)
s = "0"*(9-len(a)) + a
return "%s/%s/%s" % (s[:3], s[3:6], s[6:9])
This just pads the number to have length 9, then splits the substrings.
That being said, it seems the modulo answer is a bit better.

Categories