Cover V02, I04
Listing 1
Listing 2
Sidebar 1


Which Day of the Week Is This?

Larry Reznick

I've recently been frustrated by the fact that while you can tell cron to do something by specifying almost any form of time or date, including the day of the week, you can't tell it to do something only on the third Tuesday or the first Thursday of the month. Solving this could come in handy, since it would allow reports to be printed or data accumulated on certain days of the month, such as every other Friday, but not every Friday. Using such a program, I could schedule scripts or send mail based on the instance of this weekday in this month, not simply on the fact that it is one day of the week.

The First Solution

A simple and fast approach occurred to me. Dividing a month's day number by 7 produces two useful numbers. The quotient equals which week this day is in. The remainder equals the day of the week this particular day is, when the day of the week of the first day of the month is known.

For any division the quotient could be 0, so a quotient greater than 0 represents the second quotient. Thus, you'll add 1 to the quotient to represent that fact.

A special case arises when the day of the week is evenly divisible by 7. In this case, adding 1 makes the quotient advance by one week a day too soon, from the algorithm's point of view. To correct for that problem, do not add 1 when the day number is evenly divisible by 7 (i.e., when the remainder is 0).

The weekdaynum script (Listing 1), which expresses the division-by-seven algorithm, executes in the Bourne shell, as designated by the first line's reference to /bin/sh. weekdaynum requires at least one command-line argument -- the weekday number you want today to be. If today is that weekday number, the weekdaynum program will set the exit status to TRUE. Anything else sets the status to FALSE.

If there aren't enough arguments on the command line, the exit status is explicitly set to whatever the false program delivers.

Once the program is certain that at least one argument is present, it assigns that argument to the whichday variable. It is extremely important to make this assignment right away because the script uses one of the handiest features of shell script programming: the ability of the set command to parse a line into its component words. Those words are placed in the positional parameters, $1, $2, $3, and so forth. The special shell variables, $#, $*, and $@ will all be changed to fit the characteristics of the new positional parameters. Because the results are put into the positional parameters, everything that came from weekdaynum's command line must be collected before doing the set; otherwise, the new settings will destroy the original command-line data. In the command

set `date`

set parses the output of the current date and time so that $3 becomes the day of the month.

All that remains of this simple script is to apply the division-by-seven algorithm. Before running the main division, the program needs to know whether to add 1. The test program delivers a 0 if TRUE and a 1 if FALSE. If the current weekday number is evenly divisible by seven, test delivers a 0. If the weekday number isn't evenly divisible by seven, test delivers a 1. Either way, that value must be added to the quotient. The shell script assigns the test's exit status to the flag variable. That variable's value is added to the quotient. Finally, test the quotient against the weekday number requested on the script's command line. If they are identical, today must be the day. The exit status of the weekdaynum script doesn't need to be set explicitly by the exit command. The status of the last command executed within a script becomes the status of the whole script.

Using weekdaynum

As leader of a C Special Interest Group, I want to send CSIG members a reminder message one week before the meeting, on the second Tuesday of every month. The crontab can isolate Tuesdays by specifying 2 in the day of the week field. So, if the mail is to be sent at 1:15 A.M. on a Tuesday, the crontab line would appear as

15 1 * * 2 weekdaynum 2 && mail -s "CSIG Upcoming" \

csig < /usr/local/doc/csigmail

This command line takes advantage of the shell's Boolean AND logical operator. If the first part of an && is TRUE, the second part executes. If the first part is FALSE, the second part will be ignored. cron makes sure this is done only on Tuesdays. The weekdaynum program checks to see if today is the correct Tuesday. If today is not the correct Tuesday, the && prevents the mail program from being run.

The shell's Boolean OR logical operator can be used, too. If the first part of an || is TRUE, the second part is ignored, because an OR is true when either part is true. If the first part is FALSE, the second part will be executed to see if it is TRUE or not. You can use the || operator with the weekdaynum script when you want to run something every day of the week except a particular day.

For example, assume that you want a weekly system backup done every Saturday except the first Saturday. On the first Saturday, you want a master backup made. Use two crontab lines:

1 5 * * 6 weekdaynum 1 && /usr/local/bin/masterbu
1 5 * * 6 weekdaynum 1 || /usr/local/bin/weeklybu

If this is a Saturday, both those lines will kick in at 5:01 A.M. But the first line, using the &&, will run the masterbu script only if this is the first Saturday. If the first part of && is TRUE, the second part must be executed. The second line, using the ||, will run the weeklybu script only if this is not the first Saturday. If the first part of || is TRUE, the second part won't be executed.

Finally, if you want to run something on the second and fourth Thursday of every month:

31 18 * * 4 (weekdaynum 2 || weekdaynum 4) \

&& /usr/local/bin/semimo.thu

The parentheses surrounding the || operation forces the || to take precedence over the && operator. If today is the second Thursday, the test for today being the fourth Thursday won't be done, but the entire parenthetical condition will be TRUE. If today is not the second Thursday, the || requires that weekdaynum check to see if today is the fourth Thursday. If it is, the parenthetical condition will be TRUE. Only if both parts are FALSE will the parenthetical || be FALSE, and if this is the case, then the first part of the && will also be FALSE. If the first part of the && is FALSE, the second part won't run. However, if the parenthetical condition is TRUE, the second part of the && must run the special script.

Which Is the 3rd Wednesday?

After I finished the weekdaynum script, it occurred to me that some future programs would want to know which day of the current month is a particular day of the week. Using the same numbers as cron, a program or user could query such a program for either the weekday's name, or the weekday's number. Listing 2 shows that Bourne shell program, weekdate.

Some useful shell functions appear at the beginning of the weekdate script (such functions can simplify the code greatly, even when the functions are apparently simple).

The first shell function shows a usage output. Since this program is expected to be used interactively and within a program, it's worth taking the time to give some idea of its usage.

The calprog function isolates the variation between SCO's cal command and other UNIX cal commands. SCO's cal command shows the current month, and also the previous and next month. Every other UNIX I've seen or worked with shows only the current month. To get SCO's cal program to show only the current month, you must ask for it explicitly. calprog uses the date program to figure out the current month number and year, then submits them to the cal program. On SCO, the calprog function's output is identical with the output of cal on other UNIX systems.

The daycolumn function takes one argument. The $1 in the function refers not to the weekdate program's command line, but to the daycolumn function's argument list. The pipeline in the daycolumn function takes the output of the calprog function, the current month's calendar, and runs it through tail to isolate the lines containing day numbers. The first line of calprog's output shows the name and year of the month, and the second line shows the one-letter abbreviations for the days of the week. By taking the output starting with the third line, tail passes only the day numbers through the pipe. The pipe delivers the data to the cut program. cut uses daycolumn's first argument as a character range, isolating the column containing a specific day of the week. Code appearing later in this script determines which column that will be.

Getting the Work

First, as usual, is a test to see that the command line has the right number of arguments. There must be two: the count of the day of the week (1 to 5) and which day of the week (Sun to Sat). For instance,

weekdate 2 Thu
weekdate 4 mon
weekdate 5 3

where the last example is the numeric way to refer to the fifth Wednesday.

If the argument count on the command line is correct, the whichone and whichday variables are assigned the arguments. Those variables must be tested for validity. The whichone variable must be within the range 1 to 5 or it is nonsense, and the program must quit. If it is 5, it may still be nonsense, but the script will discover that later. Validating whichday is a little tougher, since either a day name or a day number is acceptable.

To make Sun equivalent to 0, Mon equivalent to 1, and so on, seven variables with those names are arranged. By naming the variables after the days, a numeric value can be directly translated from the day name used on the command line.

The tr program converts the whichday variable's value entirely to lowercase. That makes comparison easier, relates the day name directly to the equivalent variable name, and lets the user type either uppercase or lowercase.

The case statement isolates the acceptable day values. If whichday is an acceptable number, it is taken on face value. If it is the name for a day of the week, case converts it to a number for the calprog to use, by the following line:

whichday=`eval echo $"$whichday"`

The backticks start a substitution subshell for the eval program to execute in. eval will execute the echo command, but first the $whichday inside quotation marks must be resolved. That becomes the already validated day name, and the quotation marks can now be stripped, leaving the $ in front of the day name! In other words, if Thu was the string on the weekdate command line, $thu will be the argument given to the echo command. This eval technique creates an associative array of variable names. echo can now deliver the appropriate number as the replacement value for the whichday variable.

If the whichday variable contains any other value, it must be a usage error. Otherwise, the whichone and whichday variables are now valid and regularized, and they can be used to isolate the daycolumn in the calendar.

Slicing the Calendar

To use the daycolumn function, a field range must be set up for the cut program to use. The calprog output is very regular. Each day's month numbers are in specific character positions, three characters per day: two for the right-justified number and one for the space separating the columns. The Saturday column doesn't actually have three characters, but the cut program won't mind that.

A range is represented by two numbers separated by a hyphen. The first number is the first character of the column. The second number is one character after the last character in the column. cut's ranges begin with 1, not 0, so 1 must be added to the first position of the range. Thus, 1-2 would deliver the first column, 4-5 the second, and so on.

The first character position is the whichday number, multiplied by 3 using the expr command, and the second position is 2 more than that. The field variable is used as a temporary variable to hold the first character position. It is used twice more to create the final format needed by cut. The result is stored back in the field variable.

Special Problems

Every month except February will have at least one column with five entries in it, and February is exempt only when it begins on a Sunday. So, any day of the week can be a fifth day, but not all months will have a particular fifth day. A look at the calendar's daycolumn tells if the request for a fifth day is meaningful for the current month. With the field variable set, the daycolumn function can deliver the column for this test. daycolumn pipes its data to the wc program, since the number of words corresponds to the number of days in the column. If there are fewer than five days, the program repeats the command line used and shows the relevant calendar so that the user can verify what is really possible.

One final problem appears in the layout of the calendar: the first row of the calprog output doesn't necessarily contain all seven days of the first week. The only time the first row contains the entire first week is when the first day of the month falls on a Sunday, as it does in August 1993. The worst case happens when the first day of the month falls on a Saturday. This happens in May 1993, when all but one day is in the second row of the calprog output. If the user requests the first day, it might come before the first actual day of the month if the program blindly looks in the first row. The program must adjust itself to look in the second row.

As with the field variable, setting the firstday variable is a two-step process. Its first-step value gets used in the second step, so firstday acts as its own temporary variable. The first is to find out how many days are in the first row of the calendar -- the first calendar week. calprog output is then sent to tail to eliminate the heading lines, just as the daycolumn function does. The head program truncates the remaining calendar lines, keeping only the first row. Finally, weekdate counts the number of days in that row.

The second step turns the number of days into a number compatible with the whichday variable, which is itself compatible with cron's day numbers. The easiest way to do this is to subtract the number of days in the first week from 7 (for example, if there is only one day in the first calendar week, 7-1=6, which is Saturday's day number). If the day requested, $whichday, is less than the first day of the month, $firstday, the first appearance of that day must be in the second calendar row, so 1 is added to the whichone variable.

Seize the Day

The whichone variable now corresponds to the line number in the calprog output containing the day that the user requested. The program now sets the day variable by running the daycolumn output through tail, specifying $whichone as the line to start with. It passes that to head, which keeps only the first line. Since the daycolumn function has only one number per line, and the whichone variable isolates a single line, the day variable gets only one number: the month's day number for the requested day of the week.

For the original purpose of the program, you would simply output the value of $day. However, to turn this script into another version of the weekdaynum program, use the test command to compare the $day with today's day of the month (a line doing that is commented out at the end of the script). This script is slower than the weekdaynum script. However, weekdaynum depends on cron to run it only on an appropriate day of the week. Any program can run weekdate to figure out whether this is the day to get things done.

About the Author

Larry Reznick has been programming professionally since 1978. He is currently working on systems programming in UNIX and DOS. He teaches C language courses at American River College in Sacramento. He can be reached via email at: rezbook!