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.
Basic Example
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 [7] _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 ;
Comments
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
Additional Holidays
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 ;
Historical Accuracy
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
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 ;
Usage Examples
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 ;
Custom Intervals
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 ;
Caveats
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.
... View more