BookmarkSubscribeRSS Feed

SAS Tip: Generating Holiday Lists

Started ‎05-10-2019 by
Modified ‎05-10-2019 by
Views 9,187

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.

Comments

Great article, thnanks!

As for Easter, another way is to use the holiday function as with other holidays:

 

HoliDate = holiday('Easter', Year);

Version history
Last update:
‎05-10-2019 10:52 AM
Updated by:
Contributors

sas-innovate-2024.png

Available on demand!

Missed SAS Innovate Las Vegas? Watch all the action for free! View the keynotes, general sessions and 22 breakouts on demand.

 

Register now!

Free course: Data Literacy Essentials

Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning  and boost your career prospects.

Get Started

Article Labels
Article Tags