Generating Holiday Lists
Counting the working or trading days within a calendar interval is a fairly common task. It typically entails exclusion of Saturdays, Sundays, and holidays. The Saturdays and Sundays can be readily identified using the WEEKDAY function. SAS has a HOLIDAY function, but it is not analogous to the WEEKDAY function.
A solution is to use rules to generate holiday lists.
The following example reflects United States Government holidays, but can easily be adapted to other rule sets. Note that holidays occurring on weekends are shifted to adjacent weekdays. Typically that is appropriate for applications requiring identification of trading days or working days.
data holidays ; length HolidayName $ 30 ; array WkDayShift  _temporary_ ( 1 5*0 -1 ) ; retain ShiftToggle /* 0 */ 1 ; do Year = 1971 to 2070 ; HolidayName = "New Year's Day" ; * 1 Jan if not Mon 2 Jan. ; HoliDate = holiday('newyear', year) ; dow = weekday(HoliDate) ; HoliDate = intnx( 'day', HoliDate, WkDayShift[dow] * ShiftToggle ) ; if not ( (dow EQ 7) and ShiftToggle ) then output ; HolidayName = "Martin Luther King Day" ; * 3rd Mon in Jan ; HoliDate = holiday('mlk', year) ; output ; HolidayName = "Presidents' Day" ; * 3rd Mon in Feb ; HoliDate = holiday('uspresidents', year) ; output ; HolidayName = "Memorial Day" ; * Last Mon in May ; HoliDate = holiday('memorial', year) ; output ; HolidayName = "Independence Day" ; * 4 Jul if not Mon 5 Jul or Fri 3 Jul ; HoliDate = holiday('usindependence',year) ; HoliDate = intnx('day', HoliDate, WkDayShift[ weekday(HoliDate) ] * ShiftToggle) ; output ; HolidayName = "Labor Day" ; * 1st Mon in Sep ; HoliDate = holiday('labor', year) ; output ; HolidayName = "Columbus Day" ; * 2nd Mon in Oct ; HoliDate = holiday('columbus', year) ; output ; HolidayName = "Veterans Day" ; *11 Nov if not Mon 12 Nov or Fri 10 Nov ; HoliDate = holiday(ifc(ShiftToggle, 'veteransusg', 'veterans'), year) ; output ; HolidayName = "Thanksgiving Day" ; *4th Thu in Nov ; HoliDate = holiday('thanksgiving', year) ; output ; HolidayName = "Christmas" ; * 25 Dec if not Mon 26 Dec or Fri 24 Dec ; HoliDate = holiday('christmas', year) ; HoliDate = intnx('day', HoliDate, WkDayShift[ weekday(HoliDate) ] * ShiftToggle) ; output ; HolidayName = "New Year's Eve" ; * Fri 31 Dec or not at all ; HoliDate = mdy(12, 31, year) ; if weekday(HoliDate) EQ 6 and ShiftToggle then output ; end ; keep year HolidayName HoliDate ; format HoliDate weekdate17. ; run ;
- The array WkDayShift specifies the shifting of holidays from weekends to weekdays. It maps Sunday to Monday, Saturday to Friday, and each weekday to itself.
- The effective constant ShiftToggle turns such shifting on (1) and off (0).
- New Year's day is a special case. If January 1 is a Saturday, it is shifted to (Friday) December 31, of the preceding year. In order to preserve calendar-year grouping in that situation, the observation is not output. To compensate for that, a New Year's Eve holiday is generated, but only when it falls on a Friday.
- The INTNX function is used to implement weekend-to-weekday shifting for New Year's Day, Independence Day, and Christmas. For Veterans Day, the HOLIDAY function for some reason supports such shifting (one simply specifies "veteransusg" instead of "veterans"), so the code is simpler.
The results for selected years:
proc print data=holidays ; where year in (1971, 1972, 2070) ; by year ; id year ; run ;
Year HolidayName HoliDate 1971 New Year's Day Fri, Jan 1, 1971 Martin Luther King Day Mon, Jan 18, 1971 Presidents' Day Mon, Feb 15, 1971 Memorial Day Mon, May 31, 1971 Independence Day Mon, Jul 5, 1971 Labor Day Mon, Sep 6, 1971 Columbus Day Mon, Oct 11, 1971 Veterans Day Thu, Nov 11, 1971 Thanksgiving Day Thu, Nov 25, 1971 Christmas Fri, Dec 24, 1971 New Year's Eve Fri, Dec 31, 1971 1972 Martin Luther King Day Mon, Jan 17, 1972 Presidents' Day Mon, Feb 21, 1972 Memorial Day Mon, May 29, 1972 Independence Day Tue, Jul 4, 1972 Labor Day Mon, Sep 4, 1972 Columbus Day Mon, Oct 9, 1972 Veterans Day Fri, Nov 10, 1972 Thanksgiving Day Thu, Nov 23, 1972 Christmas Mon, Dec 25, 1972 2070 New Year's Day Wed, Jan 1, 2070 Martin Luther King Day Mon, Jan 20, 2070 Presidents' Day Mon, Feb 17, 2070 Memorial Day Mon, May 26, 2070 Independence Day Fri, Jul 4, 2070 Labor Day Mon, Sep 1, 2070 Columbus Day Mon, Oct 13, 2070 Veterans Day Tue, Nov 11, 2070 Thanksgiving Day Thu, Nov 27, 2070 Christmas Thu, Dec 25, 2070
The HOLIDAY function currently (as of SAS Version 9.2) recognizes only widely observed United States and Canadian holidays. Other holidays can be generated using the appropriate functions. For holidays observed on a fixed day of the week, such as Patriot's Day, use the NWKDOM function. For holidays with fixed dates, it's the MDY function. Examples:
HolidayName = "Patriots' Day" ; * 3rd Mon in Apr ; HoliDate = nwkdom(3, 2, 4, year) ; output ; HolidayName = "St. Patrick's Day" ; * 17 Mar ; HoliDate = mdy(3, 17, year) ; output ;
The HOLIDAY function will generate any holiday it recognizes for any year which SAS recognizes. For example, note in the results above that Martin Luther King Day appears for 1970 and 1971 even though it was not adopted as an official holiday until 1986. Veteran's Day is another such case; from 1971 through 1977 it was observed on the fourth Monday in October before reverting in 1978 to the traditional November 11, yet the HOLIDAY function yields November 11 for all years. Whether these are problems depends on the time horizons of one's application. If necessary, conditional code can be added to adjust or suppress the incorrect results of the function.
HolidayName = "Martin Luther King Day" ; * 3rd Mon in Jan 1986- ; HoliDate = holiday('mlk', year) ; if year GE 1986 then output ; HolidayName = "Veterans' Day" ; * 4th Mon in Oct 1971-1977 ; if year IN (1971 : 1977) then HoliDate = nwkdom(4, 2, 10, year) ; else HoliDate = holiday(ifc(ShiftToggle, 'veteransusg', 'veterans'), year) ; output ;
Techniques for SAS Version 9.1
Both the HOLIDAY and NWKDOM functions were introduced in Version 9.2. Generating fixed-date holidays for earlier versions is rather straightforward using the MDY function. See the example for St. Patrick's Day, above. Fixed-day-of-week holidays are a bit more complicated. Here are two examples:
HolidayName = "Thanksgiving Day" ; * 4th Mon in Nov ; HoliDate = intnx('week.5', mdy(11, 7*4, year), 0) ; output ; HolidayName = "Memorial Day" ; * Last Mon in May ; HoliDate = intnx('week.2', intnx('month', mdy(5, 1, year), 0, 'end'), 0) ; output ;
In the first, Thanksgiving is defined as the start of the Thursday-to-Wednesday week which includes November 28. In the second, Memorial Day is defined as the start of the Monday-to-Sunday week which includes the last day of the month of May.
Easter presents a more difficult problem, since it is not defined within the context of the Gregorian calendar. One must instead turn to Computus. Here is a solution derived from the 1876 Nature article.
HolidayName = "Easter" ; * No simple Gregorian formula ; a = mod(Year, 19) ; b = floor(Year / 100) ; c = mod(Year, 100) ; d = floor(b / 4) ; e = mod(b, 4) ; f = floor( (b + 8) / 25 ) ; g = floor((b - f + 1) / 3) ; h = mod(19*a + b - d - g + 15, 30) ; i = floor(c / 4) ; k = mod(c, 4) ; L = mod( (32 + 2*e + 2*i - h - k), 7) ; m = floor( (a + 11*h + 22*L) / 451) ; month = floor( (h + L - 7*m + 114) / 31) ; day = mod(h + L - 7*m + 114, 31) + 1 ; HoliDate = mdy(month, day, Year) ; output ;
Similar and presumably equivalent code can be found on the SAS web site.
Displaying a Calendar
Given a HOLIDAYS data set (see above), one can call PROC CALENDAR to display it, as with
ods html ; ods listing close ; proc calendar data=holidays fill weekdays ; where year EQ 2020 ; start holidate ; run ; ods listing ; ods html close ;
Consider the WEEKDAY date interval. It supports date interval computations which skip weekend days (by default, Saturday and Sunday; other definitions available to reflect local custom). Often one needs to refine that behavior by skipping weekday holidays and possibly unplanned weekday shutdowns.
SAS supports Custom Time Intervals for this purpose.
Suppose you have the HOLIDAYS data set (see above), and you want to establish a custom business day interval which will support dates from 2008 to 2010 inclusive. Start by generating a list of weekdays which include the specified 3-year span.
data Mon_Fry ; do MTuWThF = '01Jan2008'd-10 to '31Dec2010'd+10 ; if weekday(MTuWThF) in (2:6) then output ; end ; format MTuWThF weekdate17. ; run ;
To generalize a bit, suppose that there were some unexpected business closures during those years. Record those in a separate data set.
data unplanned ; input Event $ Event_Date date9. ; format event_date weekdate17. ; cards ; PowerOut 09Sep2008 Blizzard 06Mar2009 Blizzard 07Mar2009 ;
Now complete the custom interval definition by excluding the holidays and other closure dates from the weekday enumeration.
data active_days(keep=begin) ; merge Mon_Fry (rename=(MTuWThF =Begin) in=weekday ) holidays (rename=(holidate =Begin) in=holiday ) unplanned(rename=(event_date=Begin) in=unplanned) ; if weekday and not (holiday or unplanned) ; by begin ; run;
The INTERVALDS= System Option implements and names the custom interval definition.
options intervalds=(BusinessDay=active_days) ;
Here's an application example. It generates a list of business days, excluding weekends, holidays, and other closures.
data demo ; DemoDay = '01Jan2008'd ; do until(DemoDay GT '31Dec2010'd) ; output ; DemoDay = intnx('BusinessDay',DemoDay,1) ; end ; format DemoDay weekdate17. ; run ;
- Holidays vary from place to place, from time to time, and from institution to institution.
- There can be additional loss of working and trading days as a result of ad hoc and unscheduled closures.
An Expanded Holiday Function
Sometimes One Needs an Option with Unusual Dates provides code that combines the holiday function, PROC FCMP and the intervalds option to create an expanded holiday function (holiday_x) that includes Chinese, Islamic and Jewish holidays, as well as additional holidays and spelling variants.