Wednesday 5 January 2011

SAS Macro functions and returning values and a bit about macro variable type casting

I wrote an article about this subject before but I was quite new to SAS and it was quite confusing so I'm writing it again better and clearer this time.

The SAS macro programming language is a strange language lacking data types we usually associate with strongly or even weakly typed programming languages. SAS macro variables are intrinsically treated as strings. The equivalent command for casting a type from one to another is an input() command. For example, if I have a string that contains the month and year (MMMYY), then I can convert it to a SAS date format like this:

%let mmmyy = JAN11;
%let datum = %sysfunc(input(&mmmyy., MONYY.)); 
*the second argument defines the format of the data (&mmmyy.) that you want to cast into SAS format. &datum. now holds the SAS date equivalent of 01/01/2011;

Datum now holds the SAS date format of the first of January 2011. SAS macro variable values like the integer date stored in datum are intrinsically stored as strings, so you can't just do anything with them. In order to treat them as an actual integer and do any mathematical operations on them you need to use eval() like so:

%let yesteryear = %eval(&datum. - 1); *holds the SAS date for 31/12/2010;

One problem often encountered with SAS macro variables is when working with dates. Often, people define a date like so:

%let bankholiday = "03JAN11"d;

This is acceptable, but if you try to do any mathematical operations using that variable outside of a data step it will throw an error message and literally show that you tried to carry out an operation on the string ' "03JAN11"d '. The best ways to convert such a date to the SAS date format is to use either intnx() or define the date using mdy():

%let bankholiday = %sysfunc(mdy(01, 03, 11));
or
%let bankholiday = %sysfunc((intnx(day, "03JAN11"d, 0)));

I've yet to figure out how to detect the type of the macro variable before I do anything with it, so I advise you to inform other programmers about the type of the variable your code works with before you let someone else use your code.

Now. When it comes to macro functions, SAS also proves to be strange when compared to other languages. What is a very natural and normal way of working in a procedural/Object Oriented language appears to be missing in SAS. I've yet to see an official documented example that shows a macro returning a value, but it is possible. I doubt I'm the first person to discover or use this method.

What is odd about macro functions that return values is that you should NEVER use single line comments inside the body of the macro otherwise it throws an error! Always use the multi-line comments like /* this comment */. And there is no "return" command preceding the variable to make it return but there is a %return statement and all this does is stop the execution of the rest of the code and jump out of the macro. The way to return a value is to just write the the name of the variable holding the value you wish to return, without a semicolon on the end.

Anyway. Here's an example bit of code that lets you ask if a date is a weekend or not. If it is a weekend it returns 1 else it returns 0.

/**
* %isWeekend(datum)
* Tells you if a date is a weekend or not by returning 1 (true) or 0 (false). If you leave it empty it will check todays date.
* Usage:
%let datum = %sysfunc(mdy(01,01,2011));
%put %isWeekend(&datum.); *shows 1 (true);
*
* @return    boolean    If the date you supplied is a weekend it will return 1, otherwise 0.
* @param    date    date    The date you want to check. If you leave this empty it will use todays date
* @date 20110105
* @author Ahmad Retha
**/

%macro isWeekend(date);
    %if date=  %then %do;
        %let date = %sysfunc(today());
    %end;

    %let wd = %sysfunc(weekday(&date.)) ; /*find the weekday of the date given. Sunday=1, Saturday=7*/

    %let iwe = 0; /*set the variable we wish to retune to 0 (false) initially*/
    %if &wd.=1 or &wd.=7 %then %do;
        %let iwe = 1;
    %end;

    /* return iwe (1 or 0) */
    &iwe.
%mend;


Allow me to explain what this code does. The first part of the code, the %if statement, checks to see if a date argument was supplied and if it isn't it uses today's date. This behaviour of setting an empty parameter to a default value makes our macro function more robust and easy to use - it is a recommended practice.

The next section assigns the weekday, Sunday, Monday, Tuesday through to Saturday to the variable &wd. as a number from 1 to 7, as there are 7 days in a week. In SAS, the week starts on Sunday, 1, and ends on Saturday, 7.

Next we create a macro variable, iwe, which holds the value we wish to to return. We want to return 1 (true) if the date is a weekend, or 0 (false) if it's not. Initially we set the value to 0 (false) as most days of the week return false.

The next part checks if the weekday is Sunday (1) or Saturday(7) and if the date's day is a weekend it sets iwe to 1 (true). If it's not a weekend then iwe already holds 0 (false).

Finally, we return the value held in iwe (either 1 or 0) by just writing the variable &iwe. by itself - note that you should NOT put a semicolon on the end otherwise it will throw an error. There is no "return" command - just put the variable name on a line of its own.

That's it. Easy right?

You can write many really useful macro functions that you will use often and stick them into a file and make a library of useful functions to %include and use in your projects. For example, for my job I wrote a macro called %isWeekend(date), like the one above (though as I'm writing this from home I couldn't just copy and paste and I wrote the above on the fly from memory), another macro called %getNext(day) which returns the date of the next weekday you give it, %getLast(day) which is similar but looks backwards, %isHoliday(date) and %getLastWorkday(date). I stuck those into a SAS file, a library, and now include it into my other scripts when I need the functionality. This approach promotes code re-use and makes coding quicker and easier and as there is less duplicate code it is easier to maintain. Naturally, all my code is highly documented and I recommend you comment up your code as well.

I hope this little tutorial proves useful.

- Ahmad Retha

5 comments:

  1. That is a great one Ahmad.

    Can I use this in data statements to create a new data column ?

    ReplyDelete
  2. The answer is yes you can. But it's been over a year since I wrote any SAS so I can't with absolute confidence recall the correct syntax to do it but I'll give it a shot:

    If I wanted to create a new field in a dataset and call the %isWeekend() macro function above:

    %let myvar = %isWeekend();
    data datasetname;
    set datasetname;
    new_var = "&myvar.";
    end;

    Hope that works for you.

    ReplyDelete
  3. To display the result, I use the %put %isWeekend(%sysfunc(today())); command. Why doesn't it display anything?

    ReplyDelete
  4. I don't have SAS to test this or find a solution so I'm sorry I can't be of much help. Try assigning it to a variable first before put-ing it. Or try it from within a data block.

    ReplyDelete
  5. Thank you very much for this info. It feels like SAS can sometimes be a little quirky: I try to understand what's the logic of not having a semicolon after the returnvalue or why a single line of comment does throw an error in this case ... But it is what it is and I'm glad I found your very clear explanation.

    ReplyDelete