I'm trying to write a macro code where i've used several keyword parameters and want one of those parameters to be able to read in multiple arguments/values.
I want to achieve something like this:
%MACRO TEST(CONDITION=, VVAR=, OUT_VAR=)/PARMBUFF;
%LET CNT = %sysfunc(countw(&syspbuff));
&OUT_VAR = .;
%DO I =1 %TO &CNT;
%IF &CONDITION=Y %THEN %DO;
&OUT_VAR=(SALARY+BUNUS)/COUNT(VALUES PASSED TO VVAR PARAMETER);
%END;
%END;
%MEND;
data person;
input SALARY BONUS COND $;
datalines;
100 50 Y
200 75 Y
300 0 N
;
%TEST(CONDITION=COND,VVAR=SALARY BONUS,OUT_VAR=AVG_SAL);
RUN;
Can anyone suggest how can I achieve that? I tired using the syspbuff options to read in the values for VVAR parameter, but it has all the values passed to all the parameters.
Thanks!
You appear to be confused about the timing of when macro code executes and when the code that the macro has generated is executed by SAS. The macro processor does its work first and then passes the code onto SAS to interpret. When SAS sees a complete data or proc step then it runs that step.
Here is my attempt to translate your post into something that could execute. Not sure if it is what you are looking for.
First you have a macro that takes in three parameters. The first is some SAS expression that evaluates to a string. The middle is a variable list. And the last is a single variable name. It uses these parameter values to generate SAS code that could be used inside of a data step to conditionally generate the mean of the variables in the list.
%MACRO TEST(CONDITION=, VVAR=, OUT_VAR=);
IF &CONDITION='Y' THEN DO;
&OUT_VAR=mean(of &vvar);
END;
else &out_var=.;
%MEND;
Now you will need to call that macro as part of a data step so that the code is generated in a place where SAS will understand it. Note that when your data step is using inline data (CARDS/DATALINES) then the inline data most be the last thing in the data step.
data person;
input SALARY BONUS COND $;
%TEST(CONDITION=COND,VVAR=SALARY BONUS,OUT_VAR=AVG_SAL);
datalines;
100 50 Y
200 75 Y
300 0 N
;
If you run it with the MPRINT option on you can see the SAS code that the macro has generated. It is just the same as if you had typed that code directly into the data step instead of asking the macro to generate it for you.
615 data person;
616 input SALARY BONUS COND $;
617 %TEST(CONDITION=COND,VVAR=SALARY BONUS,OUT_VAR=AVG_SAL);
MPRINT(TEST): IF COND='Y' THEN DO;
MPRINT(TEST): AVG_SAL=mean(of SALARY BONUS);
MPRINT(TEST): END;
MPRINT(TEST): else AVG_SAL=.;
618 datalines;
NOTE: The data set WORK.PERSON has 3 observations and 4 variables.
NOTE: DATA statement used (Total process time):
real time 0.06 seconds
cpu time 0.03 seconds
622 ;
If you wanted to generate multiple new variables then just call it multiple times. You could probably make the macro much more complicated by treating each input parameter as a delimited list of values and process the first set and then the second set etc. But why?
If you want to know how many words appear in the value of a macro variable, like the VVAR parameter in the macro above, then you can use the %sysfunc() macro function to call the SAS function countw().
%let cnt=%sysfunc(countw(&vvar));
You could then use the value of &cnt where you need it. Say as the upper bound in a macro %do loop or as an integer constant in an expression in the generated SAS code.
Related
I have two values which represent dates:
a=101 and b=103
Below is first macro saved in separate file one.sas:
%global time nmall;
%let nmall =;
%macro pmall;
%do i=&a. %to &b;
%if &i =&a. then %do;
%let nmall=&nmall.&i;
%end;
%else %let nmall=&nmall.,&i;
end;
%put (&nmall);
%mend;
%pmall;
So above pmall give me values 101,102,103.
Below is second macro:
%include “one.as”;
%macro c(a=,b=);
%let m=;
%let m1=;
%do i =&a %to &b;
%let o=&i;
proc sql;
create table new&o as select * from data where nb in(&o.);quit;
%let m =&m.date&o;
data date&o.;
set date&o.;
if pass =&o.;
run;
proc sort data=date&o.;
by flag;
end;
data output &a._&b.;
set &m;
%mend;
The above macro creates three datasets date101 date102 and date 103, then append it to output101_103.
I am trying to modify above macros in such a way that I will not use %macro and %mend approach. Below is the modified macro code:
data a_to_c;
do o=&a to &c;
output;
end;
run;
so above code will have values 101 102 103 in variable o for dataset a_to_c.
data _null_;
set a_to_c;
call execute ('create table new’||strip(o)||' as select * from data
where nb in(’||strip(o)||' );quit;’);
run;
I want to know how to do below things.
Create pmall values in a macro variable in my modified macro inside the data step data a_to_c, so that I can use it further.
How to proceed from %let m macro in the first macro code to new code which I am developing above.
Geetha:
I think you will find the macro-ization of the process to be far easier if you go from a data-centric explicit solution and proceed abstracting the salient features into macro symbols (aka variables)
The end run solution appears to be:
data output_101_to_103;
set original_data;
where nb between 101 and 103;
run;
proc sort data=output_101_to_103;
by nb flag;
run;
In which case you could code a macro that abstracts 101 to FIRST and 103 to LAST. The data sets could also be abstracted. The abstracted parts are specified as the macro parameters.
%macro subsetter(DATA=, FIRST=, LAST, OUTPREFIX=OUTPUT);
%local out;
%let out = &OUTPREFIX._&FIRST._&LAST.;
data &out;
set &DATA.;
where nb between &FIRST. and &LAST.;
* condition = "between &FIRST. and &LAST."; * uncomment if you want to carry along the condition into your output data set;
run;
proc sort data=&out;
by nb flag;
run;
%mend;
And use as
%subsetter (data=original_data, first=101, last=103, outprefix=output)
Note: If you did keep the condition variable in the output data, you WOULD NOT be able to use it directly as a source code statement in a future data step, as in if nb condition then ...
I suppose you could also pass the NB and FLAG as parameters -- but you approach a point of diminishing returns on the utility of the macro.
Macro-izing the specific example I showed doesn't make too much sense unless you need to perform a lot of different variations of FIRST and LAST in a well documented framework. Sometimes it is just better to not abstract the code and work with the specific cases. Why? Because when there are too many abstracted pieces the macro invocation is almost as long as the specific code you are generating and the abstraction just gets in the way of understanding.
If the macro is simply chopping up data and reassembling data, you might be better served rethinking the flow using where, by, and class statements and abstracting around that.
Pmall is macro variable which will have list of values separated by
commas. In my modify macro, i want to create pmall as macro variable
in the datastep data a_to_c; do o=&a to &c; output; end; run; – geetha
anand 1 min ago
To create a macro variable from within a data step using the CALL SYMPUTX() function.
data a_to_c;
length pmall $200 ;
do o=&a to &c;
pmall=catx(',',pmall,o);
output;
end;
call symputx('pmall',pmall);
drop pmall;
run;
If you really want to generate code without a SAS macro you can use CALL EXECUTE() or write the code to a file and use %INCLUDE to run it. Or for small pieces of code you could try putting the code in a macro variable, but macro variables can only contain 64K bytes.
It is really hard to tell from what you posted what code you want to generate. Let's assume that you want to generate an new dataset for each value in the sequence and then append that to some aggregate dataset. So for the first pass through the loop your code might be as simple as these two steps. First to create the proper subset in the right order and the second to append the result to the aggregate dataset.
proc sort data=nb out=date101 ;
where nb=101 ;
by flag ;
run;
proc append base=date101_103 data=date101 force;
run;
Then next two times through the loop will look the same only the "101" will be replaced by the current value in the sequence.
So using CALL EXECUTE your program might look like:
%let a=101;
%let c=103;
proc delete data=date&a._&c ;
run;
data _null_;
do nb=&a to &c;
call execute(catx(' ','proc sort data=nb out=',cats('date',nb,'),';'));
call execute(cats('where nb=',nb,';')) ;
call execute('by flag; run;');
call execute("proc append base=date&a._&c data=");
call execute(cats('date',nb));
call execute(' force; run;');
end;
run;
Writing it to a file to run via %INCLUDE would look like this:
filename code temp ;
data _null_;
file code ;
do nb=&a to &c;
put 'proc sort data=nb out=date' nb ';'
/ ' where ' nb= ';'
/ ' by flag;'
/ ';'
/ "proc append base=date&a._&c data=date" nb 'force;'
/ 'run;'
;
end;
run;
proc delete data=date&a._&c ;
run;
%include code / source2;
If the goal is to just create the aggregate dataset and you do not need to keep the smaller intermediate datasets then you could just use the same name for the intermediate dataset on each pass through the loop. That will make the code generation easier as then there is only only place that needs to change based on the current value. Also that way you only need to have two dataset names even for a sequence of 10 or 20 values. It will take less space and reduce clutter in the work library.
I'm new in SAS. I'd like to know what the lines below do. Couldn't figure out what it does to the program because I didn't encounter any of the defined variables in the succeeding parts after declaration.
%let cutofftime =%sysfunc(time());
%let currdt = %sysfunc(putn(&cutofftime.,time5.)) ;
%put &cutofftime. &currdt.;
The %let statement is used to create a macro variable.
The first statement:
%let cutofftime =%sysfunc(time());
uses the time() function to determine the current time. It returns the current time as a numeric value which is number of seconds since midnight.
The second statement:
%let currdt = %sysfunc(putn(&cutofftime.,time5.)) ;
uses the PUTN() to convert the numeric time value (which Is now stored in the macro variable CUTOFFTIME) to a pretty formatted value like 22:30.
So after the two %let statements have run, you created two macro variables. Then the %PUT statement is used to write the values of the two macro variables to the log:
%put &cutofftime. &currdt.;
Using the %PUT statement to write the value of macro variables to the log is a useful way to debug macro code, in the same way that the PUT statement can be used to write the value of data step variables to the log as a data step debugging tool. When I run the code at 9:32 PM, the log shows:
3 %put &cutofftime. &currdt.;
77537.809 21:32
That said, if you're new to SAS, you should probably avoid the trying to learn the macro language at the same time as you are learning the SAS language.
I want to execute a macro conditionally, based on some loop variable in the dataset.
data alldat;
do i=1 to 5;
* here, a macro should be called ;
* that is accessing some array variable of the dataset ;
* e.g. %my_macro(my_array(i));
%put hello;
output;
end;
run;
proc print; run;
How is this possible?
If I execute this example code above, hello is only output once, while alldat contains 5 values, as one would expect. I want 5 hellos in my output.
Thank you!
If you want your data step loop to output hello 5 times then use a PUT statement instead of a %PUT statement.
In general macros are used to generate SAS code that is then executed. It really depends on what type of code your macro generates. If it only generates statements than can be used inside of a data step then call it once inside of your DO loop and the generated statements will run 5 times. So if your macro generates data step statements that could update the variable whose name is passed to it then your code might look like this.
data alldata;
array my_array var1-var5 ;
do i=1 to dim(my_array);
%my_macro(my_array(i));
put 'hello';
output;
end;
run;
Otherwise you could use CALL EXECUTE to generate the macro calls so that they can then run and generate their code after the data step stops. This data step will read values from an existing array and pass the values to the macro. So it will generate 5 calls to the macro for every observation in the input data. The generated macro calls will run after the data step stops.
data _null_;
set mydata;
array my_array var1-var5 ;
do i=1 to dim(my_array);
call execute(cats('%nrstr(%my_macro)(',my_array(i),');'));
call execute('%nrstr(%put hello;)');
end;
run;
I currently have a SAS process that generates multiple data sets (whether they have observations or not). I want to determine a way to control the export procedure based on the total number of observations (if nobs > 0, then export). My first attempt was something primitive using if/then logic comparing a select into macro var (counting obs in a data set) -
DATA _NULL_;
SET A_EXISTS_ON_B;
IF &A_E > 0 THEN DO;
FILE "C:\Users\ME\Desktop\WORKLIST_T &PDAY..xls";
PUT TASK;
END;
RUN;
The issue here is that I don't have a way to write multiple sets to the same workbook with multiple sheets(or do I?)
In addition, whenever I try and add another "Do" block, with similar logic, the execution fails. If this cannot be done with a data null, would ODS be the answer?
The core of what you want to do, conditionally execute code, can be done one of a number of ways.
Let's imagine we have a short macro that exports a dataset to excel. Simple as pie.
%macro export_to_excel(data=,file=,sheet=);
proc export data=&data. outfile=&file. dbms=excel replace;
sheet=&sheet.;
run;
%mend export_to_excel;
Now let's say we want to do this conditionally. How we do it depends, to some degree, on how we call this macro in our code now.
Let's say you have:
%let wherecondition=1; *always true!;
data class;
set sashelp.class;
if &wherecondition. then output;
run;
%export_to_excel(data=class,file="c:\temp\class.xlsx", sheet=class1);
Now you want to make this so it only exports if class has some rows in it, right. So you get the # of obs in class:
proc sql;
select count(1) into :classobs from class;
quit;
And now you need to incorporate that somehow. In this case, the easiest way is to add a condition to the export macro. Open code doesn't allow conditional executing of code, so it needs to be in a macro.
So we do:
%macro export_to_excel(data=,file=,sheet=,condition=1);
%if &condition. %then %do;
proc export data=&data. outfile=&file. dbms=excel replace;
sheet=&sheet.;
run;
%end;
%mend export_to_excel;
And you add the count to the call:
%export_to_excel(data=class,file="c:\temp\class.xlsx", sheet=class1,condition=&classobs.)
Tada, now it won't try to export when it's 0. Great.
If this code is already in a macro, you don't have to alter the export macro itself. You can simply put that %if %then part around the macro call. But that's only if the whole thing is already a macro - %if isn't allowed outside of macros (sorry).
Now, if you're exporting a whole bunch of datasets, and you're generating your export calls from something, you can add the condition there, more easily and more smoothly than this.
Basically, either make by hand (if that makes sense), or use proc sql or proc contents or (other method of your choice) to make a dataset that contains one row per dataset-to-export, with four variables: dataset name, file to export, sheet to export (unless that's the same as the dataset name), and count of observations for that dataset. Often the first three would be made by hand, and then merged/updated via sql or something else to the count of obs per dataset.
Then you can generate calls to export, like so:
proc sql;
select cats('%export_to_excel(data=',dataname,',file=',filename,',sheet=',sheetname,')')
into :explist separated by ' '
from datasetwithnames
where obsnum>0;
quit;
&explist.; *this actually executes them;
Assuming obsnum is the new variable you created with the # of obs, and the other variables are obviously named. That won't pull a line with anything with 0 observations - so it never tries to execute the export. That works with the initial export macro just as well as with the modified one.
Suggest you google around for different approaches to writing XLS files.
Regarding using a DATA step or PROC step, the DATA step is tolerant of datasets that have 0 obs. If the SET statement reads a dataset that has 0 obs, it will simply end the step. So you don't need special logic. Most PROCS also accomodate 0 obs dataset without throwing a warning or error.
For example:
1218 *Make a 0 obs dataset;
1219 data empty;
1220 x=1;
1221 stop;
1222 run;
NOTE: The data set WORK.EMPTY has 0 observations and 1 variables.
1223
1224 data want;
1225 put "I run before SET statement.";
1226 set empty;
1227 put "I do not run after SET statement.";
1228 run;
I run before SET statement.
NOTE: There were 0 observations read from the data set WORK.EMPTY.
NOTE: The data set WORK.WANT has 0 observations and 1 variables.
1229
1230 proc print data=empty;
1231 run;
NOTE: No observations in data set WORK.EMPTY.
But note as Joe points out, PROC EXPORT will happily export a dataset with 0 obs and write an file with 0 records, overwriting if it was there already. e.g.:
1582 proc export data=sashelp.class outfile="d:\junk\class.xls";
1583 run;
NOTE: File "d:\junk\class.xls" will be created if the export process succeeds.
NOTE: "CLASS" range/sheet was successfully created.
1584
1585 data class;
1586 stop;
1587 set sashelp.class;
1588 run;
NOTE: The data set WORK.CLASS has 0 observations and 5 variables.
1589
1590 *This will replace class.xls";
1591 proc export data=class outfile="d:\junk\class.xls" replace;
1592 run;
NOTE: "CLASS" range/sheet was successfully created.
ODS statements would likely do the same.
I use a macro to check if a dataset is empty. SO answers like:
How to detect how many observations in a dataset (or if it is empty), in SAS?
I am new to SAS and having a hard time figuring out when should the simple If-Then-else and when should %IF-%THEN-%ELSE should be used. As an example code below:
%let inFile = %scan(&sysparm, 1, " ");
%macro read_data(infile);
data want;
infile "&infile" LRECL=1000;
retain fdate;
if _n_ = 1 then do;
input Agency $ Status $ Num $ fdate sdate;
end;
else do;
%if fdate < 20130428 %then
input
#1 poolno $6.
#7 factor 9.8 #;
%else
input
#1 rectype $1
#3 poolno $6.
#9 factor 9.8 #;
#18 pfactor 9.8;
output;
end;
drop Agency Status Num sdate;
run;
proc print data=want;
run;
%mend read_data;
%read_data(&inFile);
I am trying to get the first line(header) and taking the parameter fdate. Based on the value of this parameter, I am parsing the subsequent input lines differently.
But this does not seem to work, as only the second input part runs (always getting parameter 'rectype' in output).
Any suggestions as what i might be doing wrong?
I see you have C++ as one of your tags, and that you are just getting started with SAS. So I'll try to provide an answer specific to your background.
The easiest way to understand the distinction between SAS Macro commands and the commands in the DATA step or in several procs with the same name, like %if vs. if, is to think of SAS Macro commands as equivalent to C/C++ pre-processor (CPP) directives. CPP and SAS MAcro are both macro languages, and although they are not exactly the same kind of language, they share two important and initially confusing characteristics: they are text processors; and they are executed as a separate step before the main code is processed.
There are places where this approximation breaks down, but as a beginner to SAS with background in C/C++, it is a good place to start.
The macro statement %if is compiled before any data step statement. That means you are generally not able to use a data step variable in your logic expression. When the macro processor is compiling the macro statement, data step variable doesn't exist yet.
In the example above, the %IF condition is based on a datastep variable/value. This should indicate it can be achieved using a datastep 'if' rather than a %IF.
You have already received an answer to this in your previous question, https://stackoverflow.com/a/15341502/108797
If you do %if fdate < 20130428 SAS compares literals fdate and 20130428 not
the value od fdate and 20130428.
If you had a macro variable named fdate you would do %if &fdate < 20130428.
In your case fdate is a variable in a dataset, so use if not %if, but it seems like you are trying to create a data step with a macro so just using if will probably not work in this case (depends on what are you trying to get).