Passing macro function as argument to another macro function - sas

I am new to SAS and I am playing around with macros.
I want to define a function returning a truth value and pass it as argument to another function where it is used in an %IF. As an example (not working):
%MACRO truth(datum);
%PUT datum < 100;
%MEND;
%MACRO use_truth(table, condition);
%PROC SQL;
SELECT *
FROM table
WHERE %condition(number);
QUIT;
%MEND
%use_truth(table1 , truth) ;
Where table1 is an SQL table with some column number.
How can I make such a procedure work?

One important thing to remember is that macros are just text substitution, and they're run before any of your code executes. Macros don't return values as such, they just return code.
When you run WHERE %&condition(number); (note the ampersand to actually resolve the value of the macro variable) what the proc SQL actually ends up seeing is WHERE %PUT number < 100;
Here's how you could do what you'd like, but be aware that the 'truth' macro isn't actually returning a yes/no value; it's just writing that text into the proc SQL.
%MACRO truth(datum);
&datum < 100
%MEND;
%MACRO use_truth(table, condition);
PROC SQL;
SELECT *
FROM &table
WHERE %&condition(number);
QUIT;
%MEND;
%use_truth(testInput, truth);
If you're dealing with more complicated logic, I'd suggest looking into proc FCMP. That'll let you define custom functions that can be used in data steps and proc SQL.

A macro that is generating code inline to be used as part of another statement can not itself cause any sort of step to occur (Proc or Data)
You can pass a macro name to another macro, and invoke it after resolving it.
%macro dispatch (macro_name);
%&macro_name
%mend;
%macro a;
%put NOTE: Aaaaaaay!;
%mend;
%dispatch (a)

Related

SAS Macro Conditional Statement not Creating Table

when I run the sas code below, it does not generate the table. What am I doing wrong? Thanks.
%macro createData(test=);
%if 2+2 = 4 %then %do;
proc sql;
create table test as
select
*
from datasets.mydata
;
quit;
%end;
%mend;
That code runs fine as long as you actually call the macro!
You need one more line:
%createData(test=1);
(test=1 is arbitrary, since you don't do anything with the &test macro parameter I just picked whatever I wanted).
SAS Macros are similar in other programming languages to methods - they don't do anything until they're called, running the macro definition itself just compiles the macro and gets it ready to be used.

SAS macro modification

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.

How to mask an & in a SAS macro variable call via an indirect reference

I'm using the following macro code to generate a list of clients from a table:
%macro listclient;
proc sql noprint;
select unique(client)
into :cli1 -
from books; quit;
%put Total Number of Clients: &sqlobs..;
%do i=1 %to &sqlobs;
%put Client &i &&cli&i;
%end;
%mend listclient;
%listclient
My problem is that some of the clients have names such as Smith & Jones, so I need to use some sort of masking function to get a correct resolution. I've tried a few things, my best guess being to use %nrbquote(&&cli&i) but can't seem to null the problem out. I imagine that I am making a syntax error, or that there may be an issue with the indirect macro variable referencing.
The code runs but with a warning every time an & is encountered in the client name.
I would prefer not to go down the route of replacing all of the &s with "and"s and then changing them back again!
Can anybody be of assistance?
A useful macro function for this is %SUPERQ(). You just need to provide it with the name (not the value) of the macro variable to be quoted.
%do i=1 %to &sqlobs;
%put Client &i %superq(cli&i);
%end;
You could also build your macro variable to already be formatted as quoted strings. If you use single quotes then the macro triggers inside will be ignored.
proc sql noprint;
select distinct cats("'",tranwrd(name,"'","''"),"'")
into :name1 -
from sashelp.class
;
quit;
%put &=NAME1;
NAME1='Alfred'

SAS - How to return a value from a SAS macro?

I would like to return a value from a SAS macro I created but I'm not sure how. The macro computes the number of observations in a dataset. I want the number of observations to be returned.
%macro nobs(library_name, table_name);
proc sql noprint;
select nlobs into :nobs
from dictionary.tables
where libname = UPCASE(&library_name)
and memname = UPCASE(&table_name);
quit;
*return nobs macro variable;
&nobs
%mend;
%let num_of_observations = %nobs('work', 'patients');
Also, I would like the &nobs macro variable that is used within the macro to be local to that macro and not global. How can I do that?
I'll answer the core question Bambi asked in comments:
My main concern here is how to return a value from a macro.
I'm going to quibble with Dirk here in an important way. He says:
A SAS macro inserts code. It can never return a value, though in some cases you can mimic functions
I disagree. A SAS macro returns text that is inserted into the processing stream. Returns is absolutely an appropriate term for that. And when the text happens to be a single numeric, then it's fine to say that it returns a value.
However, the macro can only return a single value if it only has macro statements in addition to that value. Meaning, every line has to start with a %. Anything that doesn't start with % is going to be returned (and some things that do start with % might also be returned).
So the important question is, How do I return only a value from a macro.
In some cases, like this one, it's entirely possible with only macro code. In fact, in many cases this is technically possible - although in many cases it's more work than you should do.
Jack Hamilton's linked paper includes an example that's appropriate here. He dismisses this example, but that's largely because his paper is about counting observations in cases where NOBS is wrong - either with a WHERE clause, or in certain other cases where datasets have been modified without the NOBS metadata being updated.
In your case, you seem perfectly happy to trust NOBS - so this example will do.
A macro that returns a value must have exactly one statement that either is not a macro syntax statement, or is a macro syntax statement that returns a value into the processing stream. %sysfunc is an example of a statement that does so. Things like %let, %put, %if, etc. are syntax statements that don't return anything (by themselves); so you can have as many of those as you want.
You also have to have one statement that puts a value in the processing stream: otherwise you won't get anything out of your macro at all.
Here is a stripped down version of Jack's macro at the end of page 3, simplified to remove the nlobsf that he is showing is wrong:
%macro check;
%let dsid = %sysfunc(open(sashelp.class, IS));
%if &DSID = 0 %then
%put %sysfunc(sysmsg());
%let nlobs = %sysfunc(attrn(&dsid, NLOBS));
%put &nlobs;
%let rc = %sysfunc(close(&dsid));
%mend;
That macro is not a function style macro. It doesn't return anything to the processing stream! It's useful for looking at the log, but not useful for giving you a value you can program with. However, it's a good start for a function style macro, because what you really want is that &nlobs, right?
%macro check;
%let dsid = %sysfunc(open(sashelp.class, IS));
%if &DSID = 0 %then
%put %sysfunc(sysmsg());
%let nlobs = %sysfunc(attrn(&dsid, NLOBS));
&nlobs
%let rc = %sysfunc(close(&dsid));
%mend;
Now this is a function style macro: it has one statement that is not a macro syntax statement, &nlobs. on a plain line all by itself.
It's actually more than you need by one statement; remember how I said that %sysfunc returns a value to the processing stream? You could remove the %let part of that statement, leaving you with
%sysfunc(attrn(&dsid, NLOBS))
And then the value will be placed directly in the processing stream itself - allowing you to use it directly. Of course, it isn't as easy to debug if something goes wrong, but I'm sure you can work around that if you need to. Also note the absence of a semi-colon at the end of the statement - this is because semicolons aren't required for macro functions to execute, and we don't want to return any extraneous semicolons.
Let's be well behaved and add a few %locals to get this nice and safe, and make the name of the dataset a parameter, because nature abhors a macro without parameters:
%macro check(dsetname=);
%local dsid nlobs rc;
%let dsid = %sysfunc(open(&dsetname., IS));
%if &DSID = 0 %then
%put %sysfunc(sysmsg());
%let nlobs = %sysfunc(attrn(&dsid, NLOBS));
&nlobs
%let rc = %sysfunc(close(&dsid));
%mend;
%let classobs= %check(dsetname=sashelp.class);
%put &=classobs;
There you have it: a function style macro that uses the nlobs function to find out how many rows are in any particular dataset.
What is the Problem writing function-like macros?
i.e. macros you can use as%let myVar = %myMacro(myArgument)
You can use your user written macro as if it were a function if all you do is
calling some %doSomething(withSometing) like macro functions
assign values to macro variables with a %let someVar = statement
"return" your result, typically by writing &myResult. on the last line before your %mend
As soon as you include a proc or data step in your macro, this does not work any more
Luckily, %sysFunc() comes to the rescue, so we can use any data step function
This includes low level functions like open, fetch and close which can even access your data
nerdy people can do quite a lot with it, but even if you are nerdy, your boss will seldom give you the time to do so.
How do we solve this?, i.e. which building blocks do I use to solve this?
proc fcmp allows packaging some data step statements in a subroutine or function
This function, meant for use in a data step, can be used within %sysfunc()
Within this function you can call run_macro to execute any macro IN BACKGROUND IMMEDIATELY
Now we are ready for the practical solution
Step 1: write a helper macro
with no parameters,
using some global macro variables
"returning" its result in a global macro variable
I know that is bad coding habit, but to mitigate the risk, we qualify those variables with a prefix. Applied to the example in the question
** macro nobsHelper retrieves the number of observations in a dataset
Uses global macro variables:
nobsHelper_lib: the library in which the dataset resides, enclosed in quotes
nobsHelper_mem: the name of the dataset, enclosed in quotes
Writes global macro variable:
nobsHelper_obs: the number of observations in the dataset
Take care nobsHelper exists before calling this macro, or it will be ost
**;
%macro nobsHelper();
** Make sure nobsHelper_obs is a global macro variable**;
%global nobsHelper_obs;
proc sql noprint;
select nobs
into :nobsHelper_obs
from sashelp.vtable
where libname = %UPCASE(&nobsHelper_lib)
and memname = %UPCASE(&nobsHelper_mem);
quit;
%* uncomment these put statements to debug **;
%*put NOTE: inside nobsHelper, the following macro variables are known;
%*put _user_;
%mend;
Step 2: write a helper function;
**Functions need to be stored in a compilation library;
options cmplib=sasuser.funcs;
** function nobsHelper, retrieves the number of observations in a dataset
Writes global macro variables:
nobsHelper_lib: the library in which the dataset resides, enclosed in quotes
nobsHelper_mem: the name of the dataset, enclosed in quotes
Calls the macro nobsHelper
Uses macro variable:
nobsHelper_obs: the number of observations in the dataset
**;
proc fcmp outlib=sasuser.funcs.trial;
** Define the function and specity it should be called with two character vriables **;
function nobsHelper(nobsHelper_lib $, nobsHelper_mem $);
** Call the macro and pass the variables as global macro variables
** The macro variables will be magically qouted **;
rc = run_macro('nobsHelper', nobsHelper_lib, nobsHelper_mem);
if rc then put 'ERROR: calling nobsHelper gave ' rc=;
** Retreive the result and pass it on **;
return (symget('nobsHelper_obs'));
endsub;
quit;
Step 3: write a convenience macro to use the helpers;
** macro nobs retrieves the number of observations in a dataset
Parameters:
library_name: the library in which the dataset resides
member_name: the name of the dataset
Inserts in your code:
the number of observations in the dataset
Use as a function
**;
%macro nobs(library_name, member_name);
%sysfunc(nobsHelper(&library_name, &member_name));
%* Uncomment this to debug **;
%*put _user_;
%mend;
Finally use it;
%let num_carrs = %nobs(sasHelp, cars);
%put There are &num_carrs cars in sasHelp.Cars;
Data aboutClass;
libname = 'SASHELP';
memname = 'CLASS';
numerOfStudents = %nobs(sasHelp, class);
run;
I know this is complex but at least all the nerdy work is done.
You can copy, paste and modify this in a time your boss will accept.
;
A SAS macro inserts code. It can never return a value, though in some cases you can mimic functions, usually you need a work around like
%nobs(work, patients, toReturn=num_of_observations )
** To help you understand what happens, I advice printing the code inserted by the macro in your log: ;
options mprint;
We pass the name of the macro variable to fill in to the macro, I find it most practical to
not require the user of my macro to put quotes around the libary and member names
make the name of the variable a named macro variable, so we can give it a default;
%macro nobs(library_name, table_name, toReturn=nobs);
Make sure the variable to return exists
If it exists it is known outside of this macro.
Otherwisse if we create it here, it wil by default be local and lost when we leave the macro;
%if not %symexist(&toReturn.) %then %global &toReturn.;
In the SQL, I
use the SASHELP.VTABLE, a view provided by SAS on its meta data
add the quotes I omitted in the macro call ("", not '': macro variables are not substituted in single qoutes)
use the macro %upcase function instead of the SAS upcase function, as it sometimes improves performance;
proc sql noprint;
select nobs
into :&toReturn.
from sashelp.vtable
where libname = %UPCASE("&library_name.")
and memname = %UPCASE("&table_name.");
quit;
%mend;
Pay attention if you call a macro within a macro, Run this code and read the log to understand why;
%macro test_nobs();
%nobs(sashelp, class); ** will return the results in nobs **;
%nobs(sashelp, shoes, toReturn=num_of_shoes);
%let num_of_cars = ;
%nobs(sashelp, cars, toReturn=num_of_cars);
%put NOTE: inside test_nobs, the following macro variables are known;
%put _user_;
%mend;
%test_nobs;
%put NOTE: outside test_nobs, the following macro variables are known;
%put _user_;
You can't 'return' a value from a function-style macro unless you have written it using only macro statements. Quentin's link provides an example of how to do this.
For example, you cannot use your macro like so, because proc sql cannot execute in the middle of a %put statement (this is possible with other more complex workarounds, e.g. dosubl, but not the way you've written it).
%put %nobs(mylib,mydata);
The best you can do without significant changes is to create a global macro variable and use that in subsequent statements.
To create a macro variable that is local to the originating macro, you have to first declare it via a %local statement within the macro definition.
I know I am very late to this discussion, but thought of commenting since I came across this. This is another way of doing this I think:
%macro get_something_back(input1, input2, output);
&output = &input1 + &input2;
%mend;
data _test_;
invar1 = 1; invar2 = 2;
%get_something_back(invar1, invar2, outvar);
end;
This will also work outside a datastep.
%global sum;
%macro get_something_back(input1, input2, outvar);
%let &outvar = &sysevalf(&input1 + &input2);
%mend;
%get_something(1, 2, sum);

SAS: How to assign the output value of a function-like macro?

I am very new using SAS and Im having hard time trying to assign the output value of a function-like macro to a macro variable. After testing, I have check that the value is computed correctly, but once I tried to assign it the program crashes. here you can find the code
%MACRO TP_BULLET(ZCURVE,TAU,YF=1);
/* KEEP ONLY THE ZERO CURVE UNTIL MATURITY*/
DATA _TEMP;
SET &ZCURVE;
IF MATURITY > &TAU THEN DELETE;
RUN;
PROC SQL NOPRINT;
SELECT DISTINCT 1- DF
INTO :NUME
FROM _TEMP
GROUP BY MATURITY
HAVING MATURITY = MAX(MATURITY);
QUIT;
PROC SQL NOPRINT;
SELECT SUM(DF)
INTO :DENO
FROM _TEMP;
QUIT;
PROC DELETE DATA=_TEMP;RUN;
%LET TP = %SYSEVALF(&YF*&NUME / &DENO);
&TP
%MEND TP_BULLET;
%MACRO TESTER2;
%LET K = %TP_BULLET(ZCURVE,TAU,YF=1);
%PUT .......&K;
%MEND TESTER2;
%TESTER2;
The error I am getting is the following
WARNING: Apparent symbolic reference DENO not resolved.
WARNING: Apparent symbolic reference NUME not resolved.
ERROR: A character operand was found in the %EVAL function or %IF condition where a numeric operand is required. The condition was:
1*&NUME / &DENO
So I suppose that the DATA Step is failing to create the sas table _TEMP, but I have no idea how to solve it. Thanks in advance for any help or sugestion
This is NOT a 'function like' macro - you are mixing SAS Macro language and Base SAS. Remember that the SAS Macro language is a code generator - and you are generating code, which is currently something like:
%let K=data _temp; set ZCURVE; ....
note how the above is not what you wanted to assign to the macro variable K.
To help with this, try running your macro with options mprint - this will show you the code being generated by your macro.
If you want your macro to act like a function, then (at a minimum) you should find NO code being generated via the mprint option..
All philosophical issues aside, you could add a parameter to your macro that specifies the new macrovariable (mv) that you want to create. So instead of
%Let k = %TP_BULLET(ZCURVE,TAU,YF=1);
you could call
%TP_BULLET(ZCURVE,TAU,mvOutput=k,YF=1);;
Your macro would need to be modified slightly with
%MACRO TP_BULLET(ZCURVE,TAU,mvOutput,YF=1);
%GLOBAL &mvOutput;
........ Same code as above .........
%Let &mvOutput = &TP; *Instead of final line with '&TP';
%MEND;
It is not a very SAS-y way to accomplish it, but it can help keep things more modular and comprehensible if you're working with more programming backgrounds, rather than SAS.