Friday, May 27, 2011

SAS Macro编码的好习惯之二:SAS宏设计问题

原文地址:www2.sas.com/proceedings/sugi27/p067-27.pdf

我们已经学会了如何编写SAS宏代码,但什么时候我们应该用SAS宏来编码以解决我们的问题呢?这篇文章主要考虑了两个重要的问题:一个好的宏应该具备哪些特征,以及如何才能写出一个有用且清晰的宏。

1 关于宏变量
宏变量的定义有很多种方法,如%let等,前面的文章已有介绍,这里就不多说,我主要讲一下作者的一些比较好的编写宏的理念。下面从一个简单的宏赋值语句说起:
%let data = lib.mydata ;
这个语句就是生成一个宏变量data,其值为lib.mydata。在这里要提到的就是SAS宏的编译原理,这个在前面已有介绍。SAS宏编译器前所有的代码都当作文本来处理,因此上面的宏变量data的值lib.mydata是没有特殊意义的,因此也不用加双引号” ”。但是在SAS过程步编译器里,这些值是有意义的,比如lib.mydata中的lib是库名,mydata是表名。因此,另一个例子中:
libname lib "c:myprojectdat" ;
这里的库lib的值c:myprojectdat是要加双引号的。

这里要提到两个好的习惯,其实前面的文章已有介绍:首先是选择变量名时,一定是通过这个名字就知道这个变量的功能,例如前面的data表示是数据,lib变量表示是库名等。其次,变量的值也要完整的给出来,这样增加程序可读性。

第一个好习惯大家应该都能理解,现在说一下第二个好习惯。这里我们举一个反例,例如日期是由月+空格+日+空格+年(DATE = "Jan 10 2002")组成,我们要转换成SAS日期(DATE = "10Jan2002"),我们可以用sasdate = input ( date , date9. );这个input函数来完成。但在本例中,由于我们的是月日年,而SAS日期是日月年,因此我们先得将原来的DATE的内容换一下次序:scan(date,2)||scan(date,1)||scan(date,3),因此可以用下面的解决方法:
%let x = scan(date, ;
sdt=input(&x 2)||&x 1)||&x 3),date9.);
上面的代码可以实现我们将DATE转换成SAS日期格式的功能,但是代码却非常难读懂,甚至会觉得这代码是不是乱写的。我们很难清晰明了的知道sdt的值。引外,这里的变量x的名字也不好,根本不明白它的真正意义。

由于SAS过程步编译器对代码有很多限制,而SAS宏编译器将代码视为文本,因此,在编写SAS宏的时候更容易编写出一些不具可读性,但却能正确执行的代码。

至于SAS宏变量或参数的作用,这个大家都能理解。这里还提到一点,就是双引号和单引号在SAS宏编译时的区别:SAS宏编译的时候,如果你用单引号的话,那么word scanner就将引号内的文本作为单个token,因此此表达式将不进入宏相关的变量或工具,而双引号它自已变是token,因此它将作为宏相关的变量或工具。

另一个例子是当我们要对同一目录或同一服务器下的多个文件或目录进行操作时,最好能将这同一目录和其它文件分开引用,而不是单个引用。例如:
filename in
  "\rk2vol2302statdatds1.txt" ;
filename pgm
  "\rk2vol2302statprogs" ;
libname stat
  "\rk2vol2302statsasdat" ;
这里只有三个,可能会有更多,因为太多所以很容易引起错误,因此,我们可以定义两个变量,ROOT 和 DSNUM,这样,代码就成了:
%let root = "\rk2vol2302stat";
%let dsnum = 1 ;
filename in
  "&rootdatds&dsnum..txt";
filename pgm
  "&rootprogs" ;
libname stat
  "&rootsasdat" ;
这个例子要注意几个要点,一是"&rootdatds&dsnum..txt";这一名的两个点”.”,这个以前的文章也讲过,当库为引用,后面接文件名时,一定要用两个点。然后一点是为什么"&rootprogs" ; 或"&rootsasdat" ;后面不加点呢,因为“”已经起到点的作用了,并且让我们清楚地知道sasdat还是一个目录。下面是一个反例,来说明为什么用“”而不用点:
%let root = \rk2\vol2302stat ;
filename pgm
  "&root.progs" ;
这里我们就不清楚progs到底是目录还是文件名。

另外,我们对于变量,有时称为变量,有时称为参数,那这两者有什么区别呢。其实参数呢,就是宏里面基本不变的一些变量,参数对宏来说有着重要的作用。而变量,则会经常变化其值,以满足宏的功能需求。

最后宏变量要提到一个函数”%put”,它的用途是将宏变量的值输出到log中,这对宏编程时debug非常有用。下面介绍一下几个常见的应用
%put data=&data ;        输出自定义宏变量data的值
%put _all_ ;                         输出所有宏变量及其值
%put _user_ ;            输出用户自定义变量及其值


2 宏
宏就是参数化的SAS代码。下面是一个简单的例子
libname lib "c:myprojectdat" ;
%let data = lib.mydata ;
title3 "The data set is &data" ;
proc print data = &data ;
run ;
我们可以将其定义为完整的宏:
%macro tprint (data=&syslast, tl=3) ;
title&tl "The data set is &data" ;
proc print data = &data ;
run ;
title&tl ;
%mend  tprint ;
这个宏名字叫tprint,有两个参数data和tl。这里我们来说明一些编写宏的好习惯。

首先是宏名字要能明白地说明该宏的作用(如tprint为打印)
其次考虑好选哪些变量作为宏参数(不能太多,也不能太少),以及它们的名字(data和tl:清晰明白)
最后是要指定一些好的默认值(tl=3,因为3基本能满足大多数人需求,1太小,10太大)
另外,对于title,footnote一定要记得删掉。对于libname,我们一般在代码最开始就定义好,而不是在宏内定义。

下面讲一个重要的概念,就是SAS宏编译时的时间顺序
1. Macro compile time                宏编译时间
2.  Macro execution time           宏执行时间
3.  SAS compile time                SAS过程步编译时间
4.  SAS execution time              SAS过程步执行时间
理解了宏编译的时间顺序,对于理解SAS宏代码及其功能有着重要的作用。

这里作者用了一个print宏的例子来讲解如何编一个好的程序,比较简单,我就不多作介绍了,里面提到一点是它说的jiggle test,就是每次改变一点点参数或代码,来看结果的变化,从而对宏进行调试和debug。


3 关于宏的选择语句:
那就是%if condition %then consequent ;语句。我们先来讲讲%if与if的区别吧,好像这两个都可以在SAS宏里用到,但是什么时候用呢,这就要用到上面说的SAS宏编译时的时间顺序了。%if是在宏编译时间和宏执行时间时用到,而if则是在SAS过程步编译时间和SAS过程步执行时间里用到。例如:
%macro mktrans (type=A) ;

  %if not %index(Monday Thrsday, &sysday)
  %then %do ;
    %put Program can only be run ;
    %put on Mondays and Thursdays ;
    %goto mexit ;
  %end ;
  ...
  data trans ;
    set &sysday ;
    %if %length(&type) > 0 %then
    %do ;
       if upcase(type) =
          "%upcase(&type)" ;
    %end ;
    ...
..run ;
%mexit:
%mend  mktrans ;
首先所有的代码进入宏编译器编译,然后执行,这里我们假如倒数第六行中的"%upcase(&type)"=”Y”,则宏编译器编译并执行后的代码就是
data trans ;
    set &sysday ;
       if upcase(type) =”Y”
  ...
..run ;
也即只剩下SAS过程步编译可以编译的内容了,这时再进行SAS过程步编译和SAS过程步执行,这里才会再执行if语句。其它的语句如%do与do等与此类似。
对于       if upcase(type) =
          "%upcase(&type)" ;
其解释也是一样,一个是宏编译器里处理的内容,处理完后再交给SAS过程步编译器处理。具体的参见SAS编译原理

因此,作者给出的例子
if weekday(today())=2 then
   set Monday ;
else
if weekday(today())=5 then
   set Thursday ;
else
do ;
   put "Program can only be run on "
       "Mondays and Thursdays" ;
   abort ;
end ;
至少其在宏编译的时候是不会执行的。如果要让其在宏编译的时候执行,就要将代码改为:
%macro mktrans (day=1) ;
  ...
  data trans ;
    %if &day = 2 %then 
    %do ;
      set Monday ;
    %end ;
    %else
    %if &day = 5 %then 
    %do ;
       set Thursday ;
    %end ;
    %else
    %do ;
      put "Program can only be run"
          "on Mondays and Thursdays";
      abort ;
    %end ;
    ...
..run ;
%mend  mktrans ;
呵呵,我也经常这样改,因为改起来方便,但其实这个代码可读性也很差。因此作者对代码进行了进一步改进,其核心思想是:将SAS数据步编译的代码尽量放在一起,从而增加程序的可读性
%macro mktrans (day=1) ;
  %local data ;

  %if &day = 2 %then 
    %let data = Monday ;
  %else
  %if &day = 5 %then 
    %let data = Thursday ;
  %else
    %put Program can only be run ;
    %put on Mondays and Thursdays ;
    %goto mexit ;
  %end ;

  ...
  data trans ;
    set data ;
    ...
..run ;

%mexit:
%mend  mktrans ;
作者再用宏函数%index对程序作了进一步改进:
%macro mktrans () ;

  %if not %index(Monday Thrsday,
                 &sysday)
  %then %do ;
    %put Program can only be run ;
    %put on Mondays and Thursdays ;
    %goto mexit ;
  %end ;

  ...
  data trans ;
    set &sysday ;
    ...
..run ;

%mexit:
%mend  mktrans ;
%index函数的作用是第二个参数里是否有第一个参数的值,如果没有则返回0。
另外作者还提到一个问题,就是将宏里的变量尽量定义为local。这个思想也很重要,前面的文章也讲过了,就不再重复。

4 宏循环
语法:
%do variable = firstvalue %to
         lastvalue %by increment ;
...
%end ;
例如:
%macro mklist(root=ds,from=1,to=1) ;
  %local i ;
  %do i = &from %to &to ;
     &root&i
  %end ;
%mend  mklist ;
这里&root&i后面没有加分号,是因为这样我们就能产生一系列的变量如ds1,da2…




0 comments:

 
Copyright 2010 NiuNiu's Warehouse. Powered by Blogger
Blogger Templates created by DeluxeTemplates.net | Blogger Styles | Balance Transfer Credit Cards
Wordpress by Wpthemescreator
Blogger Showcase