LJ Archive

Work the Shell

Calculating Day of the Week, Finally

Dave Taylor

Issue #209, September 2011

Dave wraps up the script and leaves us with the problem of Leap Year.

As with many of the challenges we tackle in this column, the latest project has sprawled across more issues than I ever expected when I first received the query from a reader. The question seems reasonably simple: given a month, day number and day of the week, calculate the most recent year that matches those criteria.

There are some obscure and complex formulas for doing just this, but instead, I decided it'd be interesting basically to loop backward from the current year for the month in question, parsing and analyzing the output of the handy cal program.

The real challenge has been that the cal program never really was designed to produce easily parsed output, so figuring out the day of the week (DOW, as we've been abbreviating it) involves basically counting the number of leading spaces or otherwise compensating for an average month where the first day starts mid-week, not neatly on Sunday.

An algorithmic-friendly version of cal would have output where days prior to the first day of the month would be output optionally as zeros or underscores, making this oodles easier. But it isn't, so we have to compensate.

Figuring the Day of the Week

Last month, we wrapped up with a shell function that expected the day, month and year as arguments and returned the day of the week of that particular date in that month on that year. In other words, 16 May, 2011, occurs on a Monday:

      May 2011
Su Mo Tu We Th Fr Sa
1  2  3  4  5  6  7
8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

The actual return value of the function in this instance is 2, so 1 = Sunday, 2 = Monday, and so on.

Given the desired day of the week that the user specifies and a simple way to decrement the year until we hit a match coupled with the function already shown, it should be relatively easy to assemble all the pieces and create—finally—the script that details when a specific date was on a specific day of the week.

I won't republish all the code from previous months (the completed script is 83 lines long), but here's the most salient portion at the end, the section that steps back year by year to figure out which one has a matching calendar entry:

echo Looking for $weekday, $day, $month \($monthnum\) \
    starting in $mostrecent 
# now we need to loop backwards through years until a match 
year=$mostrecent
DOW=-1                  # start with a dead value 
while [ $DOW -ne $desiredDOW ]
do
  figureDOW $day $monthnum $year
# echo "> $day $month occurred on a $DOW in $year"
  year=$(( $year - 1 ))
done 
echo "Got it! $day $month occurred on a $weekday 
 ↪most recently in ${year}:"
cal $month $year

Notice that when we find a match, we not only print out what year had that date on the specified day of the week, but we also print out the calendar for that month as a visual confirmation.

A few sample runs illustrate:

$ whatyear Friday February 9
Got it! 9 feb occurred on a fri most recently in 2006:

   February 2006
Su Mo Tu We Th Fr Sa
          1  2  3  4
5  6  7  8  9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28

$ whatyear wed aug 3
Got it! 3 aug occurred on a wed most recently in 2004:

    August 2004
Su Mo Tu We Th Fr Sa
1  2  3  4  5  6  7
8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

Since we convert the day of the week name and the month name to all lowercase and then truncate anything after the first three letters, you can see that “Friday” and “wed” both work, which is a nice side benefit. Applications with more flexible input options obviously are greatly preferred and make everyone's life easier.

Something's Still Broken

One date breaks the script because it doesn't occur every year: February 29. Here's the problem in a nutshell:

$ cal feb 2010
   February 2010
Su Mo Tu We Th Fr Sa
    1  2  3  4  5  6
7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28

When we try to find a match for “29” on this calendar, there's no matching output, and the conditional tests we have in the script can't handle the empty string.

It's not pretty:

$ whatyear mon feb 29
./whatyear.sh: line 21: [: -eq: unary operator expected
./whatyear.sh: line 72: [: -ne: unary operator expected
Got it! 29 feb occurred on a mon most recently in 2010:

   February 2010
Su Mo Tu We Th Fr Sa
    1  2  3  4  5  6
7  8  9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28

You know, if we had these ugly “[” test error messages but the end result was correct, I probably could live with it, but you can see that it's matched on a February that doesn't even have a 29th day—lame.

However, fixing it might be more trouble than it's worth, and it certainly will cause us to sprawl into a subsequent column. Instead, I encourage you to grab the entire source code library from ftp.linuxjournal.com/pub/lj/listings/issue209/11090.tgz, and explore how to fix it yourself. Yes, I am punting!

Next month, I'll start on a new shell scripting challenge, and as usual, I encourage you to send me a quick e-mail note with some ideas you have on what would be compelling for us to develop or any particularly interesting scripting problems you're facing.

Dave Taylor has been hacking shell scripts for a really long time, 30 years. He's the author of the popular Wicked Cool Shell Scripts and can be found on Twitter as @DaveTaylor and more generally at www.DaveTaylorOnline.com.

LJ Archive