These programs were featured online at UnixReview.com, Shell Corner, September 2003.
Perl and AWK Holiday Determination Routines
by Bob Orlando
Shortcuts to the code (skip the descriptions for now ):
The following describes both Perl and AWK scripts developed for determining whether a given date is a holiday, or the nth business day of the month. These routines are based on holiday algorithms from Marcos J. Montes ("American Secular Holidays") and Claus Tondering ("Frequently Asked Questions about Calendars").
American Secular Holidays in Perl
Initially, I coded the algorithms in a Perl program function or subroutine, &holiday_date (in today_is.pl). Later I added another function, &load_holiday_array, to pull the necessary information from a holidays file. Lastly, I put together a third function, &business_day, to return the nth business day's date (in the form yyyymmdd) for a given year and month (yyyymm). All three functions (called in today_is.pl) were subsequently rolled up into a single Perl macro, Holiday_dates.html (Business_day, which calls holiday_dates, has proven to be as useful as the holiday routine itself for those processes that must run, for example, on the second business day of the month.)
American Secular Holidays in AWK
Calculating Business Days
bizday=`nawk -f holidays.awk -- -b 2 holidays` if [ `date "+%Y%m%d"` = $bizday ]; then echo "Today is the 2nd business day of the month." # Do whatever fiLast business day and business day offset from the last business day (negative numbers) is also available in holidays.awk. To retrieve the last business day of the month, specify the "last" option argument (optarg) for -b option (i.e., "-b last"). For the next-to-last business day of the month, provide "-b -1" as an option and optarg.
Holidays.awk is a well-behaved program in that it uses exit status to indicate success or failure. As indicated in the documentation, all options except business day (-b), returning a zero status means the program completed successfully; non-zero indicates failure. However, with the business day option, non-zero indicates success because it is the day of the month on which the business day falls. Therefore, use the holidays.awk the exit status as the test comparand:
nawk -f holidays.awk -- -b last holidays > /dev/null 2>&1 if [ $? -eq `date +%d` ]; then echo "Today is the last business day of the month." # Do whatever fiYou can also combine -b with -m and -y to return the nth business day for a given month and year. If you request a business day (positive or negative) that is not found in the month, you receive an error message, and a 0 exit status indicating an error.
For those needing only an indication that today is a given business day, you can use the -t option in conjunction with -b. For example, using Unix cron (scheduler) we combine those options to set up a job to run only on the second business day of the month with as little as the following:
00 02 2-5 * * /usr/local/bin/holidays.awk -- -b 2 -t \ || some_program > some_program.out 2>&1In this example, no holidays file is specified because we use the default, /usr/local/bin/holidays (you can change the program to point to wherever you wish to locate the file). No nawk -f is used because the first line of holidays.awk uses the shebang syntax (#!/usr/bin/nawk -f) to execute itself. (Obviously, the program must have the necessary execution permissions to run this way.) With the -t option, holidays.awk returns true or false (which is not the same as success or failure), only running the called program if the day is, indeed, the second business day of the month.
Returning The Nth Weekday (Awk Method)
fst_monday=`nawk -f holidays.awk -- -d 1.Mon`An alternative syntax is also provided: nawk -f holidays.awk -- -d 1.1
You can expand this to report the first Monday in any month and year like this.
yyyy=2005 for mm in 1 2 3 4 5 6 7 8 9 10 11 12 do nawk -f holidays.awk -- -y $yyyy -m $mm -d 1.1 doneFor the last Sunday in a month use
nawk -f holidays.awk -- -d last.SunFor those preferring a simpler syntax: If your OS recognizes the #! (shebang) syntax, you can place a #!/usr/bin/nawk -f (or gawk) at the start of holidays.awk, thereby allowing you skip the [gn]awk -f during invocation and simply call it like this,
holidays.awk -- -d last.Sun holidays.awk -- -d last.0 holidays.awk -- -d 5.0The Perl Method
Nth day capability exists in today_is.pl, (see fst_monday in today_is.pl), but essentially, for the last Friday in May, call holiday_yyyymmdd subroutine like this:
$last_friday = holiday_yyyymmdd(200405,"last",6); $last_sunday = holiday_yyyymmdd(200502, 5,0); $second_mon = holiday_yyyymmdd(200502, 2,1);Testing with Holidays.sh
Holidays.sh executes holidays.awk, providing examples of holiday and business day testing. Provided the holidays file is located properly, executing holidays.sh on June 21, 2003 displays:
Today's no holiday, get busy. :-(( 20030101 Wed. New Year's Day 20030120 Mon. M.L.King Jr. Birthday 20030526 Mon. Memorial Day 20030704 Fri. Independence Day 20030901 Mon. Labor Day 20031127 Thu. Thanksgiving Day (US) 20031128 Fri. Thanksgiving Day II (US) 20031225 Thu. Christmas Day Today is NOT the 2nd business day (20030603) of the month. Today is NOT the last business day (20030630) of the month. Today is NOT the next-to-the-last business day (20030627) of the month.As a real acid test, I include the next-to-last and last business days of every month from 2000 to 2010. The holidays.sh script concludes with a report for all holidays for the 21st century.
The Holidays File
After a brief description of holidays file layout, I'll discuss the the file itself, and see how three holidays are handled: Memorial Day, Thanksgiving Day (including the Friday after), and Christmas.
The file itself is a simple ASCII file available to to all programs. It contains values that allow the calling program to calculate holidays either by given (fixed) month and day, or by day of a given week. The general layout is as follows:
# Mm N.Day Adj Holiday name # Comments Mm = Month number (leading zeros NOT required) N.day = Nth day (1-5 and "last") "." weekday (0-6) (Not every part is required.) Adj = Can be either a +|- n days, or weekday followed immediately by a +|- n days, Holiday name = How you want it spelled out--your call. Comments = ignored.
Leading white space is ignored, as is everything following and including the octothorpe (#-sign). Here are the entries for the three holidays:
#-----------------------------------------------------------------------# # Mm N.Day.OnOrA Adj Holiday name # Comments # # -- ----------- --- -------------------------------- ----------------- # 05 last.1 Memorial Day # Last Mon in May 11 4.4 Thanksgiving Day (US) # 4th Thu in Nov 11 4.4 +1 Thanksgiving Day II (US) 12 25 Christmas Day # M-F 12 25 6-1 Christmas Day (pre-holiday obs) # Sat? Use Fri 12 25 0+1 Christmas Day (post-holiday obs) # Sun? Use MonMemorial Day
Memorial Day is the last Monday in May. In the table the month is "05" (again, leading zero is unnecessary). The last Monday is specified by the word "last" and not a 5 because the last Monday may not be the 5th Monday (there is no 5th Monday in May, 2003). Monday is identified by the 1 following the dot (".1"). This is based on the 0-6 convention for representing Sunday through Saturday.
Friday After Thanksgiving [Thurs]Day
11 4.5 Thanksgiving Day II (Friday)since the fourth Friday might not follow the fourth Thursday of a given month. Consider Thanksgiving Day, 2002--the fourth Thursday was November 28. The fourth Friday fell on the 22nd. So, to accurately capture the Friday after Thanksgiving Day, specify the same parameters for Thanksgiving, and an adjustment of +1:
11 4.5 +1 Thanksgiving Day II (Friday)Christmas
Christmas is December 25. Like New Year's Day (January 1) and Independence Day (July 4), Christmas is a fixed date. Simply specifying "12 25 Christmas Day" in the holidays file returns "yyyy1225". However, with many companies, if Christmas falls on a Saturday (day 6), the Friday before is observed by adjusting it by -1. If it falls on a Sunday (day 0), the Monday following is observed by adjusting it by +1. Hence, the three entries:
12 25 Christmas Day # M-F 12 25 6-1 Christmas Day (pre-holiday obs) # Sat? Use Fri 12 25 0+1 Christmas Day (post-holiday obs) # Sun? Use MonNew Year's Day
New Year's Day is a fixed date, January 1, and like Christmas and Independence Day, it can be observed on the Friday before a Saturday occurrence or the Monday after a Sunday occurrence simply by setting it up like the Christmas example above. However, some organizations use a post-holiday observance of New Year's Day when it falls on a Saturday simply so the holiday falls in the correct year. You can do that by specifying New Year's Day as follows:
01 01 New Year's Day # M-F 01 01 6+2 New Year's Day (post-holiday obs) # Sat? Use Mon 01 01 0+1 New Year's Day (post-holiday obs) # Sun? Also MonRemember, the "6" in our "6+2" means the actual date, January 1st, falls on a Saturday (day 6 in the 0-6 day-numbering schema), so adjust that date by +2 days (i.e. Saturday's date (01/01) plus two days (01/03).
Daylight Savings Time
04 1.0.6 Falklands ST # 1st Sun on/after Apr 6 09 1.0.8 Falklands DST # 1st Sun on/after Sep 8
The ".6" in our "1.0.6" means Standard Time (ST) begins on the first Sunday (1.0) in April that falls on or after the 6th of April. Likewise, the ".8" in our "1.0.8" means DST begins on the first Sunday in September that comes on or after the 8th of September.
Since Daylight Savings dates are not usually holidays, you can also retrieve the Daylight Savings Time dates via the -d option and bypass the need for the holidays file altogether. Here are Daylight Savings Times for the United States (begins the second Sunday in March) and the Faulklands (begins on the first Sunday on/after September 8).
holidays.awk -- -d 2.0 -m 3 holidays.awk -- -d 1.0.8 -m 9You can even set up a cron to test for Daylight Savings Time and perform some action if true.
05 00 * 03 * [ `/usr/local/bin/holidays.awk -- -d 2.0 -m 3 -t` -eq 1 ] \ && ... Some action ...
©Copyright Bob Orlando, 2003-2016
All rights reserved.
Feb. 2, 2016
by Bob Orlando