0

I have been blowing my mind over few days to do this.. (Don't ask me why I want to do it!)

How about adding two jobs like this?

    00 06 current_day-31/3 * * job
    00 18 next_day-31/3 * * job
2
  • 1
    Have you tried adding these lines yourself? If you cannot wait 3 days, try the same trick for seconds instead for hours and tell us about the results.
    – J-mster
    Jul 10, 2015 at 7:11
  • Now... Why do you want to do it?
    – muru
    Jul 10, 2015 at 10:38

2 Answers 2

2

As @Rinzwind says, you should run a script that should check if it's time to run, every 12 hours

0 0,6,12,18 * * * /path/to/script.sh

The catch is that if you divide by 36 directly, it should have to run at 00 and 12 exactly, also it will skip the first day of the year.

This script allows choosing arbitrary values for ~00 and ~36 hours

#!/bin/bash

# Global variable with result from shouldItRun()
runCommand=0
# Run at this hours
runAt00=6
runAt36=18

# There's an issue when the year changes. Since ((365*24)/36) is
# not exact, so using the cron-only solution @JEL, it will run at
# 6PM on day 365 of first year, and at 6am on day 1 of second year  

# So ...
# Year when the script started running
yearStart=2015
# Get number of days since the first year the script is running
yearCurrent=$((`date '+%Y'`-1))
daysAccumulatedUntilThisYear=0

for i in $(seq $yearStart $yearCurrent)
do
    thisYearDays=$((`date -d ''$i'-12-31' '+%j'`+0))
    daysAccumulatedUntilThisYear=$(($daysAccumulatedUntilThisYear+$thisYearDays));
    echo '>> Year '$i' ('$thisYearDays') | Accumulated : '$daysAccumulatedUntilThisYear
done

function shouldItRun {
    # Init to false
    runCommand=0
    #
    hourOfMonth=$(($(($daysAccumulatedUntilThisYear+$1+0))*24))
    hourOfDay=$(($2+0))

    # POSIBLE VALUES 24,12,0
    hoursLeft=$(( $hourOfMonth % 36 ))

    # IF 24 hours left AND time = $runAt00:XX 
    if [ $hoursLeft -eq 24 ] && [ $hourOfDay -eq $runAt00 ]
    then
        echo '>>> DAY '$1' '$2':XX ('$hourOfMonth') : '$hoursLeft
        runCommand=1
    fi

    # IF 12 hours left AND time = $runAt36:XX
    if [ $hoursLeft -eq 12 ] && [ $hourOfDay -eq $runAt36 ]
    then
        echo '>>> DAY '$1' '$2':XX ('$hourOfMonth') : '$hoursLeft
        runCommand=1
    fi
}

# Example of today at this time
dayOfYear=`date '+%j'`
hourOfDay=`date '+%H'`

    #echo '> '$dayOfYear
    #echo '> '$hourOfDay

shouldItRun $dayOfYear $hourOfDay

if [ $runCommand -eq '1' ]
then
    echo 'Run it now!!'
else
    echo 'STOP : do not run it now!!'
fi


# Example with first 20 days of year, at 06: and 18:, 
# but tested also at 00: and 12:
for i in {1..20}
do
    shouldItRun $i 00
    shouldItRun $i 06
    shouldItRun $i 12
    shouldItRun $i 18    
done
1
  • OMG way too much code here. Tell cron to run it once an hour. At top of script, get unixtime from gnu date with %s. add offset if you care about that. Div by 3600, mod 36. if !=0 then exit. Then begin your actual work on the line after that. Nov 10, 2017 at 4:44
0

For a Single Calendar Year

Use two crontab lines. Date utility output gets the day of the year (date +\%j) and bc output assigns a 0, 1 or 2 to the day with remainder arithmetic (the modulo operator). Although the configuration can be any 36 hour spread, one example configuration is to run the job at 6am on the 1 days, at 6pm the day after, which will be a 2 day, and on the 0 days do not run the job.

0 6 * * * jd=`/bin/date +\%j`; /usr/bin/test `/bin/echo "$jd"\%3 | /usr/bin/bc` -eq 1 && job
0 18 * * *  jd=`/bin/date +\%j`; /usr/bin/test `/bin/echo "$jd"\%3 | /usr/bin/bc` -eq 2 && job

You can test this on the command line with something like

jd=`/bin/date +\%j`; /usr/bin/test `/bin/echo "$jd"%3 | /usr/bin/bc` -eq 2 && echo 'yes'

Replace jd with a target day of the year, and the 2 in "-eq 2" with 0 or 1 to see the results for a given day. For example, in 2016, days of the year 1-3 and 364-366:

1%3 = 1   364%3 = 1
2%3 = 2   365%3 = 2
3%3 = 0   366%3 = 0

So, using cron, the date and test utilities, and bc as shown in the example above, the job will run at 6am on days 1 and 364 of 2016, again at 6pm on days 2 and 365, and will not run on days 3 and 366.


Corrected Version Accounting for Non-Leap Years

A generous commenter and another answer brought to light that the above answer fails for every year except a leap year (2016, 2020). This is because the remainder pattern fails to hold across the year boundary following non-leap years (365 day years). Therefore the remainder pattern needs to be coerced into a 4-year pattern, rather than a one year pattern.

The much uglier command line test for the 4-year pattern is

jd=`/bin/date +\%j`;ya=`/bin/date +\%Y`;/usr/bin/test `/bin/echo if \("ya"%4\) 4-"ya"%4+"jd"%3 else "jd"%3 | /usr/bin/bc` -eq 0 && /bin/echo 'yes'

which, if the job is to be run every 36 hours (for example, as shown, at 6am one day, 6pm the next day, then 6am the day after the next day) could be run with two crontab lines as shown in the single calendar year example, that is

0 6 * * * jd=`/bin/date +\%j`;ya=`/bin/date +\%Y`;/usr/bin/test `/bin/echo if \("ya"%4\) 4-"ya"%4+"jd"%3 else "jd"%3 | /usr/bin/bc` -eq 1 && job_to_run
0 18 * * * jd=`/bin/date +\%j`;ya=`/bin/date +\%Y`;/usr/bin/test `/bin/echo if \("ya"%4\) 4-"ya"%4+"jd"%3 else "jd"%3 | /usr/bin/bc` -eq 2 && job_to_run

Two caveats deserve mention:

  1. While this works with the stock bc version on the version of Ubuntu I'm using, the else statement relies on non-POSIX extensions to bc. If those non-POSIX extensions aren't built into your stock bc version, there is a workaround, but I'm too lazy to figure it out. Since this is "Ask Ubuntu", I feel safe assuming you're using the stock Ubuntu bc with the non-POSIX extensions.
  2. If your system is saddled with a Daylight Savings timezone, you'll actually need 6 crontab lines, two each for the date ranges affected by Daylight Savings time, accounting for the gain and loss of an hour. The date ranges can be specified with the cron fields.

Narrative Explanation

I assume familiarity with the use of crontab fields (otherwise there's always the man page). The two lines shown will run "job" at 6am and 6pm if the test expression given after the five time and date fields succeeds. The test expression succeeds as follows.

I'll explain the more complicated test expression, because the simpler expression is part of the more complicated expression.

  1. The date utility is used to stuff the day of the year (1-366) and the year (for example, 2016) into the variables "jd" and "ya":
    jd=`/bin/date +\%j`;ya=`/bin/date +\%Y`
  1. Next the output of a bc equation is tested (with /usr/bin/test). If the result of the bc calculation equals 1 ("-eq 1"), the job is run at 6am. If the result of the bc calculation equals 2, the job is run at 6pm. If the result of the bc calculation equals any other number including 0, the job is not run.

  2. Then the echo utility is used to pipe the bc equation into the bc utility.

  3. First in the bc equation is an "if" statement. If the result of bc evaluating the equation in parens after the "if", ("ya"%4), is nonzero, the next equation, 4-"ya"%4+"jd"%3, is evaluated; otherwise, the equation after "else", "jd"%3, is evaluated.

  4. In all cases, "ya" is replaced with the current year number, and "jd" is replaced with the current day number.

  5. For the "if" condition, the result is the remainder after dividing the year by 4. For 2016, the remainder is 0, so the "else" equation is calculated. For 2017, the remainder is 1, for 2018, the remainder is 2, and for 2019, the remainder is 3. So, for 2017-2019 (but not for 2016 or 2020), the equation 4-"ya"%4+"jd"%3 is calculated. In the case of 2017, the result of 4-2017%4 is 3 (4-1); in 2018, the result is 2, and in 2019, the result is 1. That result is added to the day number before dividing the day number by 3 to get the remainder used to test whether or not the job should run.

  6. Patterns of 1,2,0 resulting from the tested bc calculation for days 1-3 and 363-365 in 2017-2019 are shown here:

    2017               2018              2019
    3 + 1 % 3 = 1      2 + 1 % 3 = 0     1 + 1 % 3 = 2
    3 + 2 % 3 = 2      2 + 2 % 3 = 1     1 + 2 % 3 = 0
    3 + 3 % 3 = 0      2 + 3 % 3 = 2     1 + 3 % 3 = 1

    3 + 363 % 3 = 0    2 + 363 % 3 = 2   1 + 363 % 3 = 1  
    3 + 364 % 3 = 1    2 + 364 % 3 = 0   1 + 364 % 3 = 2
    3 + 365 % 3 = 2    2 + 365 % 3 = 1   1 + 365 % 3 = 0
  1. This shows that the pattern 0,1,2 at the end of 2017 is repeated at the start of 2018, and likewise 2,0,1 at the end of 2018 and the start of 2019. The pattern shown above under "For a Single Calendar Year" shows that the pattern at the end of 2016 and the beginning of 2020, 1,2,0, is repeated at the beginning of 2017 and the end of 2019.
7
  • look at the revision history. I didn't add any backslashes to your answer. I applied code formatting, that is all.
    – muru
    Jul 10, 2015 at 17:16
  • So, @muru, am I correct in thinking that adding the code formatting (thanks) revealed the backslashes that I had added before because, without code formatting, the backquotes sans backslashes were translated into spaces by the answer formatter?
    – JEL
    Jul 10, 2015 at 17:28
  • Stack Exchange uses Markdown for formatting, and backquotes are used for in-line code spans, so when you had unquoted backticks, the contained text was automatically added to a code span. See askubuntu.com/editing-help#code.
    – muru
    Jul 10, 2015 at 17:31
  • Leap years are 366 days long so you will have two "0" days in a row.
    – augurar
    Nov 18, 2016 at 22:59
  • @augurar, see corrected answer accounting for non-leap year boundaries.
    – JEL
    Nov 20, 2016 at 21:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .