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!reznick@csusac.ecs.csus.edu.
|