patternpythonMinor
Python library for calculating next and previous time of the cron-like scheduled task
Viewed 0 times
previousthecronlikenexttimecalculatingforpythonlibrary
Problem
I needed a task system that would let me define a cron-like rule in the database, and be able to find the next and the previous time when the task was and is supposed to happen.
I am running a web application, so ways like run a scheduled and check for every minute, or other ways around does not work for me, so I had to come up with an algorithm. It also should support more complicated jobs, like:
Example 1: Task runs every Tuesday at 3:00pm, find the previous execution time
Example 2: next presidential election day in the USA (which is, "the Tuesday next after the first Monday in the month of November"), find the next execution time (November, 3, 2020).
This was not trivial. Here is the result:
Github page with tests, documentation and more examples
Here is the example of how to find the next presidential election day (with a bit of cheating, because we find the first Monday of November and take the next day after it).
The algorithm supports multiple strategies (using days_of_month and days_of_week together is not supported). You can define every term (minutes, hours, days, days_of_week, days_of_week_num, weeks, months, years) as a list of values, range (supports step), or None (which is like a star * in cron).
So, here is how the algorithm works in a nutshell:
-
Takes the highest term (years)
-
Figures the next year that matches the rule
-
If it does, it switches to the next term (month), tries to find the next matching value. If the value is found, it switches to the next term (days), if there is no possible values, it switches back to the year, finds the next possible year that matches the rule, and
I am running a web application, so ways like run a scheduled and check for every minute, or other ways around does not work for me, so I had to come up with an algorithm. It also should support more complicated jobs, like:
Example 1: Task runs every Tuesday at 3:00pm, find the previous execution time
Example 2: next presidential election day in the USA (which is, "the Tuesday next after the first Monday in the month of November"), find the next execution time (November, 3, 2020).
This was not trivial. Here is the result:
Github page with tests, documentation and more examples
Here is the example of how to find the next presidential election day (with a bit of cheating, because we find the first Monday of November and take the next day after it).
from scheduledtask import ScheduledTask
# Finding the next presidential election day in the USA
task = ScheduledTask(minutes=[0], hours=[0], days_of_week=[0], days_of_week_num=[0], months=[11],
years=range(1848, 9999, 4))
print(task.get_next_time() + timedelta(days=1))
print(task.get_previous_time() + timedelta(days=1))The algorithm supports multiple strategies (using days_of_month and days_of_week together is not supported). You can define every term (minutes, hours, days, days_of_week, days_of_week_num, weeks, months, years) as a list of values, range (supports step), or None (which is like a star * in cron).
So, here is how the algorithm works in a nutshell:
-
Takes the highest term (years)
-
Figures the next year that matches the rule
-
If it does, it switches to the next term (month), tries to find the next matching value. If the value is found, it switches to the next term (days), if there is no possible values, it switches back to the year, finds the next possible year that matches the rule, and
Solution
In general, you want to avoid using
So, for example your function
But here, I would let the function accept any
You can do similar things with the other functions. Note that I made your docstring into a more common format. It is multi-line now and since it only starts in the second line of the string, the common indentation will be removed automatically.
Here you can use negative indexing,
That being said, I have to say that I am not a big fan of type hinting. In this case it does not add anything. (It should be obvious that
type(x) == SomeType. This is because Python relies quite often on duck-typing (If it looks like a duck, quacks like a duck...it's a duck).So, for example your function
get_biggest_value_less_or_equal_to. Theoretically, this function could take any iterable, but because of your type hints and usage of type, it can only take a list or range. Normally, you would want to use at least isinstance(iter, list), which would also allow sub-classes of lists.But here, I would let the function accept any
Iterable and use it in the normal max call. Also, it is easier to ask for forgiveness than permission, so I would just call max in a generator expression and handle the exception that no value is below the threshold with a try..except block:from typing import Iterable
def get_biggest_value_less_or_equal_to(iter: Iterable, value):
"""
Returns the biggest element from the list that is less or equal to the value.
Return None if not found.
"""
# Special case for range
if isinstance(iter, range):
if iter.start = iter.stop: # value is greater than range, return last element of range
return iter.stop-1
else: # value is less than range, return None
return None
try:
return max(x for x in iter if x <= value)
except ValueError:
# iterator empty, no element <= value
return NoneYou can do similar things with the other functions. Note that I made your docstring into a more common format. It is multi-line now and since it only starts in the second line of the string, the common indentation will be removed automatically.
def last(iter):
"""
Returns the last element from the list or range
"""
try:
return iter[-1]
except TypeError:
raise ValueError("iter must be iterable")Here you can use negative indexing,
l[len(l) - 1] is the same as l[-1]. At first I had the special case in here as well, but range is subscriptable. If you really want the last bit of performance, you can use iter.stop - iter.step. This is slightly faster:In [2]: r = range(100000000000)
In [3]: %timeit r[-1]
10000000 loops, best of 3: 155 ns per loop
In [4]: %timeit r.stop - r.step
10000000 loops, best of 3: 94 ns per loopThat being said, I have to say that I am not a big fan of type hinting. In this case it does not add anything. (It should be obvious that
iter needs to be iterable. If you still pass something that is not iterable it will raise a helpful exception like TypeError: 'int' object is not iterable for example. If you want to, you can except that as well and display an even more helpful error message instead.)Code Snippets
from typing import Iterable
def get_biggest_value_less_or_equal_to(iter: Iterable, value):
"""
Returns the biggest element from the list that is less or equal to the value.
Return None if not found.
"""
# Special case for range
if isinstance(iter, range):
if iter.start <= value < iter.stop: # Value lies within this range, return step-aware value
return value - ((value - iter.start) % iter.step)
elif value >= iter.stop: # value is greater than range, return last element of range
return iter.stop-1
else: # value is less than range, return None
return None
try:
return max(x for x in iter if x <= value)
except ValueError:
# iterator empty, no element <= value
return Nonedef last(iter):
"""
Returns the last element from the list or range
"""
try:
return iter[-1]
except TypeError:
raise ValueError("iter must be iterable")In [2]: r = range(100000000000)
In [3]: %timeit r[-1]
10000000 loops, best of 3: 155 ns per loop
In [4]: %timeit r.stop - r.step
10000000 loops, best of 3: 94 ns per loopContext
StackExchange Code Review Q#159929, answer score: 2
Revisions (0)
No revisions yet.