SAS Macro Language

SAS Macro language – это основной и, чуть ли не единственный, инструмент повторного использования кода в SAS. В данном уроке рассматриваются основные его возможности.

Простой пример макро кода:

title "The Date is &sysdate9- the Time is &systime"; 
proc print data=demo noobs; 
run;

Оператор title задает заголовок, который выводится в окне Output. Обратите внимание на строку, которая задана в этом операторе: в этой строке используются макро переменные &sysdate9 и &systime. Это встроенные в SAS макро переменные, они хранят значение даты и времени начала текущей сессии SAS (то есть ту дату и время, когда вы запустили SAS).

В результате выполнения программы в окне Output вы увидите:

7.1

Как видите, макро переменная &sysdate9 преобразовалась в строку 23SEP2016, а &systime в строку 22:15.

Пользователь может самостоятельно создавать макро переменные и присваивать им значения. Один из способов это сделать – воспользоваться макро оператором %LET. Заметьте, все макро переменные начинаются со знака ‘&’, все макро операторы со знака ‘%’.

Итак, создадим свою макро переменную и воспользуемся ею:

%let study = 13-1014;
data dm;
    set dm_raw;
    length STUDYID $ 7;
    STUDYID = "&study";
run;

Если значение переменной STUDYID должно быть одно и то же во всех датасетах в пределах данного исследования, то хорошим приемом будет задать глобальную макро переменную "&study" и пользоваться ею во всех программах.

Теперь рассмотрим пример создания простого макроса.

%macro reldays(date, rel_date, rel_days);
    &rel_days = &date - &rel_date + (&date >= &rel_date);
%mend reldays;

Создание макроса всегда начинается с макро оператора %macro и заканчивается макро оператором %mend. Данный код создает макрос reldays, в качестве параметров, принимает макро переменные date, rel_date, rel_days. Назначение макроса уже должно быть понятным: посчитать количество дней от «базовой» даты rel_date до даты date.

Применим макрос для датасета AE:

7.2

Будем иметь в виду, что все даты в датасете AE являются строковыми переменными, а макрос reldays рассчитан на использование числовых

data ae_reldays;
    set ae;
    
    if not missing(AESTDTC) then AESTDT = input(AESTDTC, is8601da.);
    if not missing(RFSTDTC) then RFSTDT = input(RFSTDTC, is8601da.);
    
    %reldays(AESTDT, RFSTDT, AESTDY)
    
    drop AESTDT RFSTDT;
run;

Итак, внутри дата степа мы вызываем макрос reldays с помощью знака ‘%’, и передаем ему такие параметры:

Как работает этот код.

Если утрировать, SAS “подставляет” на место вызова %reldays(AESTDT, RFSTDT, AESTDY) код макроса, затем, на места макро переменных “подставляет” их значения, и только потом, выполняет получившийся датастеп. В логе можно видеть, как это происходит (опция MPRINT должна быть активна)

7.3

Результатом выполнения будет датасет ae_reldays:

7.4

Мы познакомились с понятиями макро переменных и макро операторов, рассмотрели несколько примеров их использования, а также создали и применили простейший макрос. Далее углубимся еще немного в эту тему.

Macro переменные

Макро переменные могут быть локальными и глобальными.

Рассмотрим еще способы создания макро переменных и изучим области их действия на примерах.

Оператор %GLOBAL создает глобальные макро переменные с пустыми значениями. Чтобы задать этим макро переменным другие значения, можно использовать оператор %LET. Оператор %GLOBAL можно использовать как в открытом коде, так и в теле макроса – результат будет тот же.

Оператор %LOCAL создает локальные макро переменные с пустыми значениями. Этот оператор не может быть использован в открытом коде, только в теле макроса. Для переопределения значений так же используется оператор %LET.

В открытом коде мы имеем доступ только к глобальным переменным. В теле макроса доступны как глобальные, так и локальные. Локальные и глобальные макро переменные с одним и тем же именем не конфликтуют. Это значит, что в теле макроса может быть создана макро переменная с тем же именем, что и глобальная, при этом значение глобальной переменной останется неизменным, а внутри макроса SAS будет использовать значение локальной переменной.

Уже известный вам оператор %LET создает новую макро переменную, но только в том случае, если ранее не существовало переменной с таким именем, в противном случае – просто переопределяет значение уже существующей. Причем, если это тело макроса, то, сперва, оператор попытается переопределить значение локальной макро переменной, если же локальной с таким именем не существует, то переопределяется значение глобальной, и если не существует глобальной – будет создана новая локальная макро переменная.
Поскольку в открытом коде локальные переменные недоступны, оператор %LET в открытом коде создает или переопределяет только глобальные макро переменные.

Несколько слов о синтаксисе:
%let variable_name = <value>;
На месте <value> может быть как обычная строка, так и выражение содержащее другие макро переменные. Если опустить <value>, то значение макро переменной будет пусто

Например,

%let var1 = "PINK"; в результате, var1 = "PINK"

%let var2 = color; в результате, var2 = color

%let var3 = The &var2 is &var1; в результате, var3 = The color is "PINK"

Оператор %PUT позволяет увидеть в логе значение любой макро переменной, если она существует.

Рассмотрим пример, когда глобальная и локальная макро переменные имеют одно и то же имя:

*Using a Local Variable with the Same Name as a Global Variable;
%global variable1 variable2;
%let variable1 = 1;
%let variable2 = 1;

%macro test_macro;
    %put ***** Beginning TEST_MACRO *****;
    
    %local variable1 variable3;
    %let variable1 = 2;
    %let variable2 = 2;
    %let variable3 = 2;
    
    %put The values of variables inside TEST_MACRO are:;
    %put local  variable1 = &variable1;
    %put global variable2 = &variable2;
    %put local  variable3 = &variable3;
    
    %put ***** Ending TEST_MACRO *****;
%mend test_macro;

%put The values of variables in open code before submitting TEST_MACRO are:;
%put variable1 = &variable1;
%put variable2 = &variable2;
%put variable3 = &variable3;

%test_macro

%put The values of variables in open code after submitting TEST_MACRO are:;
%put variable1 = &variable1;
%put variable2 = &variable2;
%put variable3 = &variable3;

В программе:

Теперь, подумайте и определите, какими будут значения трех макро переменных variable1, variable2 и variable3 до, после и во время выполнения макроса test_macro. Теперь проверьте ваш ответ, посмотрев в лог программы:

7.5

Что еще необходимо знать о макро переменных?
Макро переменная – это всегда строка. Рассмотрим код:

%let p = 3.14;
%let radius = 50;
%let square = &p * (&radius**2);
%put square = &square;

Можно было бы ожидать, что макро переменная square будет содержать результат выполнения арифметических действий, square = 3.14 * (50**2) = 7850. Но это не так.

В логе мы увидим,

7.6

Если же мы хотим в переменной square хранить площать круга, то мы должны «заставить» SAS в процессе замещения макро переменных их значениями преобразовать строку в число и выполнить арифметические действия. Это делается с помощью макро функций %EVAL и %SYSEVALF. Подробнее о них, и о других макро функциях смотрите здесь Macro Functions.

%let square = %sysevalf(&p *(&radius**2));
%put square = &square;

Лог:

7.7

Стоит отметить, что SAS в некоторых случаях умеет автоматически выполнять это преобразование. Например, в дата степе, когда значение числовой переменной вычисляется с помощью макро переменной:

data circles;
    input circle_id $ radius;
    
    square = &p*(radius**2);
datalines;
01 50
02 23
03 65
04 35
05 15
;
run;

Тут макро переменная &p преобразовалась в число без дополнительных усилий.

Еще несколько особенностей, о которых нужно помнить, работая с макро переменными.

Рассмотрим пример.

%let date_format = date9;

data ae;
    set ae;
    
    if not missing(AESTDTC) then AESTDT = input(AESTDTC, is8601da.);
    format AESTDT &date_format..;
run;

Обратите внимание на запись format AESTDT &date_format..; Тут мы хотим наложить на переменную AESTDT, формат, который задан в макро переменной &date_format. Если мы поставим одну точку после имени: format AESTDT &date_format.; - эта точка «скушается» SASом в счет имени самой макро переменной. Выполним такой код и обратим внимание на лог:

data ae;
    set ae;
    
    if not missing(AESTDTC) then AESTDT = input(AESTDTC, is8601da.);
    format AESTDT &date_format.;
run;

Лог программы (опция SYMBOLGEN должна быть активна):

7.7

То есть, SAS просто не будет знать, что date9 - это формат.

%Macro Statement

Как и макро переменные, макросы – это, по сути, текст, который будет подставлен в код там, где макрос был вызван. Конечно, по сравнению с макро переменными, у макросов больше возможностей:

Чтобы иметь возможность воспользоваться макросом, его необходимо сначала определить. Пример того, как это делается, вы уже видели:

%macro reldays(date, rel_date, rel_days);
    &rel_days = &date - &rel_date + (&date >= &rel_date);
%mend reldays;

Поговорим подробнее о синтаксисе.
После оператора %macro в обязательном порядке следует имя макроса (валидное имя SAS). Затем в круглых скобках параметры. Параметров в определении макроса может и не быть. Заканчивается тело макроса всегда оператором %mend, имя макроса после ключевого слова mend – опционально, его можно не указывать, тем не менее, это считается хорошим стилем программирования.

Параметры бывают двух типов: позиционными (positional) и ключевыми (keywords). В примере выше использовались позиционные, поэтому при вызове этого макроса важнейшую роль играет то, в каком порядке будут заданы параметры.

Несколько примеров вызова этого макроса:

Все приведенные выше варианты абсолютно допустимы и не приведут к ошибкам на этапе компиляции программы.

Ключевые параметры задаются со знаком равно после имени параметра. Например,

%macro reldays(date=, rel_date=RFSTDTC, rel_days=);
    &rel_days = &date - &rel_date + (&date >= &rel_date);
%mend reldays;

После знака ‘=’ можно указать «значение по-умолчанию», то есть то значение, которое будет использоваться в случае, если при вызове макроса не будет указан этот параметр. Порядок, в котором вы указываете параметры при вызове макроса, здесь не важен, поскольку вы явно указываете, какому параметру присвоить данное значение.

Например,

При объявлении макроса часть параметров могут быть позиционными, а часть ключевыми. Самое главное, что при этом нужно помнить, – это то, что сначала должны быть перечислены все позиционные параметры, а потом все ключевые. Например,

%macro reldays(date, rel_date=RFSTDTC, rel_days=);
    &rel_days = &date - &rel_date + (&date >= &rel_date);
%mend reldays;

Тут у нас один параметр (date) – позиционный, а два последующих (rel_date и rel_days) – ключевые. При вызове этого макроса первым всегда должно идти значение для параметра date. Например, вызов %reldays( , rel_days=AESTDY) будет корректным, а вот %reldays(rel_days=AESTDY, RFSTDT) вызовет ошибку.

Макро операторы

Как уже было замечено ранее, макросы могут содержать некие макро операторы, которые помогают контролировать, какой текст будет сгенерирован макро процессором и подставлен на место вызова макроса.
Вот список наиболее часто используемых (полный список тут Macro Statements):

%* Macro Comment Statement
%DO Statement
%DO, Iterative Statement
%DO %UNTIL Statement
%DO %WHILE Statement
%END Statement
%GLOBAL Statement
%IF-%THEN/%ELSE Statement
%LET Statement
%LOCAL Statement
%MACRO Statement
%MEND Statement
%PUT Statement

Не так уж и много, как может показаться, тем не менее, этих операторов, в купе с макро функциями, достаточно для большинства макросов.
С операторами %GLOBAL, %LET, %LOCAL, %MACRO, %MEND, %PUT вы уже знакомы.
С операторами %DO, %DO %UNTIL, %DO %WHILE, %END, %IF-%THEN/%ELSE познакомиться не сложно, поскольку они аналогичны тем же операторам из SAS BASE. Важнее всего понять, чем они отличаются от обычных, и когда ими пользоваться.

Усовершенствуем наш макрос reldays так, чтобы можно было ним пользоваться и для строковых переменных-дат.

%macro reldays_iso(date, rel_date, rel_days, dates_iso=1);	
    %local date_num rel_date_num; 
    %if &dates_iso %then %do;	
        if not missing(&date) then date_num = input(&date, is8601da.);
        if not missing(&rel_date) then rel_date_num = input(&rel_date, is8601da.);
        format date_num rel_date_num date9.;
        
        %let date_num = date_num;
        %let rel_date_num = rel_date_num;
    %end;
    %else %do ;
        %let date_num = &date;
        %let rel_date_num = &rel_date;
    %end;
    &rel_days = &date_num - &rel_date_num + (&date_num >= &rel_date_num);
%mend reldays_iso;

Итак, начнем с самого начала.
%macro reldays_iso(date, rel_date, rel_days, dates_iso=1);
Новый макрос reldays_iso имеет 4 параметра: первые три – позиционные (как было и раньше), и четвертый, новый, dates_iso – отвечает за то, с датами какого типа будет работать макрос. По умолчанию задано значение 1, что соответствует тому, что даты &date, &rel_date являются строковыми переменными в формате iso.

Работа макроса основана на том, что в зависимости от значения параметра dates_iso мы задаём значения для новых локальных макро переменных &date_num и &rel_date_num, и считаем дни известным нам способом.

Какие же значения могут принимать эти новые макро переменные?
Если мы работаем с числовыми датами, то есть &dates_iso ne 1, то новые макро переменные будут просто равны тем, что пользователь макроса передал в качестве параметра:

%let date_num = &date;
%let rel_date_num = &rel_date;

Если же пользователь передает в качестве параметров строковые переменные с датами в формате iso и не забывает при этом задать dates_iso=1, то в датасете будут созданы новые числовые переменные, date_num и rel_date_num, которые будут хранить в себе числовой эквивалент строковых дат. Имена этих новых числовых переменных будут присвоены локальным макро переменным &date_num и &rel_date_num:

if not missing(&date) then date_num = input(&date, is8601da.);
if not missing(&rel_date) then rel_date_num = input(&rel_date, is8601da.);
format date_num rel_date_num date9.;

%let date_num = date_num;
%let rel_date_num = rel_date_num;

Таким образом, определив значения макро переменных &date_num и &rel_date_num, становится возможным выполнить оставшийся код, &rel_days = &date_num - &rel_date_num + (&date_num >= &rel_date_num);

Применим новый макрос в датасете AE и создадим переменную AESTDY:

data ae_reldays2;
    set ae;
    
    %reldays_iso(AESTDTC, RFSTDTC, AESTDY);	 
run;

Лог выполнения дата степа:

7.8

Результат:

7.9