Skip to content
 

Run cron job every N days

Let's say we want to run a job every N days, or weeks, regardless of month or year boundaries. For example, once every three tuesdays, or once every 17 days, or whatever.

Cron itself (at least the variants I have access to) has no way to specify these time periods, so it would seem this could not be done.

But there's a simple way to do it. It is based on modular arithmetic and on the fact that we know that measurement of time on Unix starts on a concrete date, which is the well-known January the 1st, 1970 (also known as "the Epoch"). For the remainder, I'm assuming UTC and a running time of midnight for simplicity; it should be easy to consider the appropriate time differences where needed.

With this kind of requirement we need to have an actual starting date for the job, that is, when it has to run for the first time, so we can use it as a starting point for the "every N days" intervals.
Once we have an actual date of first execution for our task (say, 2013-01-15, a Tuesday, at 00:00), we can divide the time passed since the Epoch until our date into groups of N days. For this first example, let's say N == 14, two weeks. With the following calculation we can see which place our starting day occupies in a period of 14 days (two weeks):

$ echo $(( $(date +%s -d "2013-01-15 00:00") / 86400 % 14 ))
11

Dividing by 86400 gives the number of days passed since the Epoch, from which the modulo 14 is calculated. The result is 11, which tells us that at any given time, performing the above calculation using the current date will yield 11 only on $startdate, of course, and on every second Tuesday (well, every 14 days, which is the same) starting from $startdate (or going backwards from $startdate, which is not important here). Simple test code to show that it's true:

#!/bin/bash
 
# starting from 2013-01-10, calculate the modulo for each day over a period of
# 40 days, checking that only the days we're interested in have modulo 11
 
begin=2013-01-10
 
for i in {0..39}; do
  curdate=$(date +%s -d "$begin + $i days 00:00")
 
  modulo=$(( curdate / 86400 % 14 ))
 
  [ $modulo -eq 11 ] && prefix="*** " || prefix=
 
  echo "${prefix}Date $(date "+%F %T (%a)" -d @$curdate) has modulo $modulo"
done

Sample run:

$ ./modcheck.sh
Date 2013-01-10 00:00:00 (Thu) has modulo 6
Date 2013-01-11 00:00:00 (Fri) has modulo 7
Date 2013-01-12 00:00:00 (Sat) has modulo 8
Date 2013-01-13 00:00:00 (Sun) has modulo 9
Date 2013-01-14 00:00:00 (Mon) has modulo 10
*** Date 2013-01-15 00:00:00 (Tue) has modulo 11
Date 2013-01-16 00:00:00 (Wed) has modulo 12
Date 2013-01-17 00:00:00 (Thu) has modulo 13
Date 2013-01-18 00:00:00 (Fri) has modulo 0
Date 2013-01-19 00:00:00 (Sat) has modulo 1
Date 2013-01-20 00:00:00 (Sun) has modulo 2
Date 2013-01-21 00:00:00 (Mon) has modulo 3
Date 2013-01-22 00:00:00 (Tue) has modulo 4
Date 2013-01-23 00:00:00 (Wed) has modulo 5
Date 2013-01-24 00:00:00 (Thu) has modulo 6
Date 2013-01-25 00:00:00 (Fri) has modulo 7
Date 2013-01-26 00:00:00 (Sat) has modulo 8
Date 2013-01-27 00:00:00 (Sun) has modulo 9
Date 2013-01-28 00:00:00 (Mon) has modulo 10
*** Date 2013-01-29 00:00:00 (Tue) has modulo 11
Date 2013-01-30 00:00:00 (Wed) has modulo 12
Date 2013-01-31 00:00:00 (Thu) has modulo 13
Date 2013-02-01 00:00:00 (Fri) has modulo 0
Date 2013-02-02 00:00:00 (Sat) has modulo 1
Date 2013-02-03 00:00:00 (Sun) has modulo 2
Date 2013-02-04 00:00:00 (Mon) has modulo 3
Date 2013-02-05 00:00:00 (Tue) has modulo 4
Date 2013-02-06 00:00:00 (Wed) has modulo 5
Date 2013-02-07 00:00:00 (Thu) has modulo 6
Date 2013-02-08 00:00:00 (Fri) has modulo 7
Date 2013-02-09 00:00:00 (Sat) has modulo 8
Date 2013-02-10 00:00:00 (Sun) has modulo 9
Date 2013-02-11 00:00:00 (Mon) has modulo 10
*** Date 2013-02-12 00:00:00 (Tue) has modulo 11
Date 2013-02-13 00:00:00 (Wed) has modulo 12
Date 2013-02-14 00:00:00 (Thu) has modulo 13
Date 2013-02-15 00:00:00 (Fri) has modulo 0
Date 2013-02-16 00:00:00 (Sat) has modulo 1
Date 2013-02-17 00:00:00 (Sun) has modulo 2
Date 2013-02-18 00:00:00 (Mon) has modulo 3

So there we have it, every second Tuesday starting from 2013-01-15. The code shown in modcheck.sh can be made generic so that values can be passed from the command line:

#!/bin/bash
 
# use: modcheck.sh [startdate yyyy-mm-dd] [period] [wanted modulo]
 
begin=$1
length=$2
wantedmod=$3 
 
for i in {0..39}; do
  curdate=$(date +%s -d "$begin + $i days 00:00")
 
  modulo=$(( curdate / 86400 % length ))
 
  [ $modulo -eq $wantedmod ] && prefix="*** " || prefix=
 
  echo "${prefix}Date $(date "+%F %T (%a)" -d @$curdate) has modulo $modulo"
done

Another test: let's say we want every fifth day starting from 2012-12-02. Let's calculate the modulo first:

$ echo $(( $(date +%s -d "2012-12-02 00:00") / 86400 % 5 ))
0

And let's verify it:

$ ./modcheck.sh 2012-12-01 5 0
Date 2012-12-01 00:00:00 (Sat) has modulo 4
*** Date 2012-12-02 00:00:00 (Sun) has modulo 0
Date 2012-12-03 00:00:00 (Mon) has modulo 1
Date 2012-12-04 00:00:00 (Tue) has modulo 2
Date 2012-12-05 00:00:00 (Wed) has modulo 3
Date 2012-12-06 00:00:00 (Thu) has modulo 4
*** Date 2012-12-07 00:00:00 (Fri) has modulo 0
Date 2012-12-08 00:00:00 (Sat) has modulo 1
Date 2012-12-09 00:00:00 (Sun) has modulo 2
Date 2012-12-10 00:00:00 (Mon) has modulo 3
Date 2012-12-11 00:00:00 (Tue) has modulo 4
*** Date 2012-12-12 00:00:00 (Wed) has modulo 0
Date 2012-12-13 00:00:00 (Thu) has modulo 1
Date 2012-12-14 00:00:00 (Fri) has modulo 2
Date 2012-12-15 00:00:00 (Sat) has modulo 3
Date 2012-12-16 00:00:00 (Sun) has modulo 4
*** Date 2012-12-17 00:00:00 (Mon) has modulo 0
Date 2012-12-18 00:00:00 (Tue) has modulo 1
Date 2012-12-19 00:00:00 (Wed) has modulo 2
Date 2012-12-20 00:00:00 (Thu) has modulo 3
Date 2012-12-21 00:00:00 (Fri) has modulo 4
*** Date 2012-12-22 00:00:00 (Sat) has modulo 0
Date 2012-12-23 00:00:00 (Sun) has modulo 1
Date 2012-12-24 00:00:00 (Mon) has modulo 2
Date 2012-12-25 00:00:00 (Tue) has modulo 3
Date 2012-12-26 00:00:00 (Wed) has modulo 4
*** Date 2012-12-27 00:00:00 (Thu) has modulo 0
Date 2012-12-28 00:00:00 (Fri) has modulo 1
Date 2012-12-29 00:00:00 (Sat) has modulo 2
Date 2012-12-30 00:00:00 (Sun) has modulo 3
Date 2012-12-31 00:00:00 (Mon) has modulo 4
*** Date 2013-01-01 00:00:00 (Tue) has modulo 0
Date 2013-01-02 00:00:00 (Wed) has modulo 1
Date 2013-01-03 00:00:00 (Thu) has modulo 2
Date 2013-01-04 00:00:00 (Fri) has modulo 3
Date 2013-01-05 00:00:00 (Sat) has modulo 4
*** Date 2013-01-06 00:00:00 (Sun) has modulo 0
Date 2013-01-07 00:00:00 (Mon) has modulo 1
Date 2013-01-08 00:00:00 (Tue) has modulo 2
Date 2013-01-09 00:00:00 (Wed) has modulo 3

So to use all this in our crons, we need to know the starting date, the frequency (every N days) and calculate the modulo. Once the modulo is known, we run the job if the modulo calculated for "now" (when the job is invoked) matches the modulo we want. So for instance if the period is 13 days and the modulo we want is 6, in our script we do:

#!/bin/bash
 
if (( $(date +%s) / 86400 % 13 != 6 )); then exit; fi
 
# run the task here
...

Or as usual it can also be done in the crontab itself so the script does not need to have special knowledge (it may not even be a script, so in that case the check would have to be external anyway):

0 0 * * *  bash -c '(( $(date +\%s) / 86400 \% 13 == 6 )) && runmyjob.sh'

Note: so far, it doesn't seem to have trouble with DST time changes. Corrections welcome.

Be Sociable, Share!

Leave a Reply

(required)