dynamically exclude column names in proc sql macro - sas

I have a proc sql statement in a macro function that selects column names from dictionary.columns. I would like to exclude column names based on multiple string patterns that are passed a arguments - see below
%symdel keepnames;
%macro test(data=, col=);
%global keepnames;
%let data_lib = %sysfunc(upcase(%sysfunc(scan("&data", 1, "."))));
%let data_data = %sysfunc(upcase(%sysfunc(scan("&data", 2, "."))));
%put &data_lib;
%put &data_data;
proc sql noprint;
select name into :keepnames separated by " "
from dictionary.columns
where libname = "&data_lib" and
memname = "&data_data" and
upcase(name) not like upcase("&col.");
quit;
%mend test;
%test(data=sashelp.cars, col=mpg w)
%put &keepnames;
Ideally, the col argument would turn into %mpg%, %w% thereby excluding any column names with mpg or w in their name.
There are a couple issues I'm encountering. First, I can't quite figure out how to hide the % from the macro processor. I tried using %str() in several ways but without luck. Second, I can't easily add % symbols around the words in the col argument. Any help is appreciated!

Change the macro parameter name to be something better informing, for example
%macro fetch_names (data=, dropPattern=, resultVar=fetchedNames)
...
%mend;
Consider passing a regular expression instead of a space separated list of values that would have to be iterated over.
%let fetchedNames = ;
%fetch_names (
data = sashelp.cars
, dropPattern = mpg|w /* <------- regular expression pattern */
, resultVar = fetchedNames
)
The innard of the macro would be similar.
change into :keepnames to into :&resultVar
change upcase(name) not like upcase("&col.") to not prxmatch("/&dropPattern./i", name)

I made use of the contains operator in the following and looped through the arguments in col to generate separate tests for each exclusion. For some reason I don't have cars so I used class:
%symdel keepnames;
%macro test(data=, col=);
%global keepnames;
%let data_lib = %sysfunc(upcase(%sysfunc(scan("&data", 1, "."))));
%let data_data = %sysfunc(upcase(%sysfunc(scan("&data", 2, "."))));
%put &data_lib;
%put &data_data;
%let i = 1;
%let exclusion = %scan(&col,&i);
proc sql noprint;
select name into :keepnames separated by " "
from dictionary.columns
where libname = "&data_lib" and
memname = "&data_data"
%do %while(&exclusion ne );
and upcase(name) not contains upcase("&exclusion")
%let i = %eval(&i + 1);
%let exclusion = %scan(&col,&i);
%end;
;
quit;
%mend test;
option mprint;
%test(data=sashelp.class, col=ame x)
%put &keepnames;
%test(data=sashelp.class)
%put &keepnames;
%test(data=sashelp.class, col=e)
%put &keepnames;

Related

PROC SQL within SAS Macro to list all variables of a data set - SELECT Statement causing error

I was trying to create a macro to output a list of all variables of a specific data set. In my macro, I am using PROC SQL. The code runs OK outside %macro, but error message saying the SELECT statement is not valid when it is being used within %MACRO
here is an example:
proc sql noprint;
select name into :vlist separated by ' '
from dictionary.columns
where memname = upcase("&dsn");
quit;
%put &vlist;
the above works perfectly;
but
%macro getvars(dsn);
%local vlist;
proc sql noprint;
select name into :vlist separated by ' '
from dictionary.columns
where memname = upcase("&dsn");
quit;
&vlist;
%mend;
the above doesn't work when I tried to do:
%let var_list = %getvars(dataset);
it returns:
ERROR 180-322: Statement is not valid or it is used out of proper order.
underlining the SELECT statement within the PROC SQL
SAS macros are not like functions in most programming languages: they don't return values, they are actually replaced by the content of the macro.
The solution is to make your macro variable global, outside the macro. Then you don't need to assign it to a new macro variable with %let.
%global vlist;
%macro getvars(dsn);
proc sql noprint;
select name into :vlist separated by ' '
from dictionary.columns
where memname = upcase("&dsn");
quit;
%mend;
%getvars(work.class)
%put &=vlist;
[EDIT]
and then just use the list in your keep statement
data OUT (keep= &vlist. VAR_B1);
merge DATA_A (in=a) DATA_B (in=b) ;
run;
Seems like the only viable option for my use case is from the following SAS paper, under the section of "USING A MACRO LOOP"
https://support.sas.com/resources/papers/proceedings/proceedings/sugi30/028-30.pdf
To clarify, my use case need a direct output of the list itself, not a macro variable.
e.g.
data OUT (keep= %getvars(DATA_A) VAR_B1);
merge DATA_A (in=a)
DATA_B (in=b)
;
run;
The PROC SQL won't work for me. So I think I need to move over to SAS I/O Functions in Macro Loop.
Below is from the SAS Paper:
%Macro GetVars(Dset) ;
%Local VarList ;
/* open dataset */
%Let FID = %SysFunc(Open(&Dset)) ;
/* If accessable, process contents of dataset */
%If &FID %Then %Do ;
%Do I=1 %To %SysFunc(ATTRN(&FID,NVARS)) ;
%Let VarList= &VarList %SysFunc(VarName(&FID,&I));
%End ;
/* close dataset when complete */
%Let FID = %SysFunc(Close(&FID)) ;
%End ;
&VarList
%Mend ;
A macro using %SYSFUNC(DOSUBL( can run any amount of SAS code (in a separate stream) when invoked at source code parse-time.
Example:
data have_A;
do index = 1 to 10;
x = index ** 2; y = x-1; z = x+1; p = x/2; q = sqrt(x); output;
end;
run;
data have_B(keep=B1);
do index = 1 to 10;
B1 + index; output;
end;
run;
%macro getvars(data);
%local rc lib mem names;
%let rc = %sysfunc(DOSUBL(%nrstr(
%let syslast = &data;
%let lib = %scan (&SYSLAST,1,.);
%let mem = %scan (&SYSLAST,2,.);
proc sql noprint;
select name into :names separated by ' ' from
dictionary.columns where
libname = "&lib." and
memname = "&mem."
;
quit;
)));
/* Emit variable name list */
&names.
%mend;
data OUT (keep=%getvars(HAVE_A) B1);
merge HAVE_A (in=a) /* 1:1 merge (no BY) */
HAVE_B (in=b)
;
run;
%let var_list = %getvars(dataset);
will resolve to:
%let var_list = proc sql noprint;
select name into :vlist separated by ' '
from dictionary.columns
where memname = upcase("dataset");
quit;
So it will store "proc SQL noprint" in var_list, and then fail because you use sql satements outside of proc sql.

return TRUE value if column exists in SAS table

I can't quite figure out how to return a TRUE value when a column exists in a table (NOTE - TRUE value can be any value which I can evaluate downstream as TRUE)
Example - Modify code between asterisks
%macro column_exists(data=, target=);
%local check;
%let data_lib = %sysfunc(upcase(%sysfunc(scan("&data", 1, "."))));
%let data_data = %sysfunc(upcase(%sysfunc(scan("&data", 2, "."))));
%put &data_lib;
%put &data_data;
proc sql noprint;
select name into :check separated by " "
from dictionary.columns
where libname = "&data_lib" and
memname = "&data_data" and
upcase(name) = upcase("&target");
quit;
* RETURN LOGICAL/NUMERIC/CHAR VALUE *
%mend column_exists;
data _null_;
%let test = %column_exists(data=sashelp.cars, target=mpg_city);
if &test eq TRUE then %put 'ok'; else %put 'no';
run;
If you want to create a "function" style macro it cannot generate ANY actual SAS code. It can only contain macro statements.
You can use %sysfunc() macro function to call SAS functions like OPEN() and VARNUM() in macro code.
%macro varexist(ds,var);
%local dsid ;
%let dsid = %sysfunc(open(&ds));
%if (&dsid) %then %sysfunc(varnum(&dsid,&var));
%else 0 ;
%let dsid = %sysfunc(close(&dsid));
%mend varexist;
See this link for the full macro definition with comments and additional features. https://github.com/sasutils/macros/blob/master/varexist.sas

Error in Macro Function

I'm using SAS Enterprise Guide. New to writing SAS macro functions. Nested a proc sql inside a macro. I'm trying to first check if a column exists and return the column number and then using the column number, to get the column name so that I can call this macro function in a query builder. However I'm getting ERROR 180-322 under the 'select cats' part of the code. Listed below is the code I have written so far:
%macro varexist(ds,var);
%local rc dsid result dynvar;
%let dsid = %sysfunc(open(&ds));
%let result = %sysfunc(varnum(&dsid,&var));
%let rc =%sysfunc(close(&dsid));
proc sql;
select cats('t1.',name) into :dynvar separated by ', '
from dictionary.columns
where libname = 'WORK' and
memname = 'TRANSPOSE_DATA' and
varnum = "&result";
quit;
&dynvar
%mend varexist;
%put %varexist(WORK.TRANSPOSE_DATA,VAR1);
You have not coded a "MACRO FUNCTION", since the macro emits multiple statements (proc sql; select ... quit;). So if you tried to use it as if it was a function like this:
%let myvar=%varexist(work.transpose_data,age);
then you will end up generating code like:
%let myvar=proc sql;
select cats('t1.',name) ... ;
quit;
So the reason the the select... generates an error is because it is not within a PROC SQL step as the PROC SQL statement has become the value assigned by the %LET statement.
As mentioned in one our my comments, I changed the logic and got it work as follows:
%macro varexist(ta,ds,var);
%local rc dsid result col_name;
%let dsid = %sysfunc(open(&ds));
%let result = %sysfunc(varnum(&dsid,&var));
%let rc =%sysfunc(close(&dsid));
%if &result > 0 %then %let col_name = &ta&var;
%else %if &ta = t1. %then %let col_name = '';
&col_name
%mend varexist;

Create a sequence of new column names

I have a hundred or so columns which I would like to rename in SAS using the following macro:
%macro rename1(oldvarlist, newvarlist);
%let k=1;
%let old = %scan(&oldvarlist, &k);
%let new = %scan(&newvarlist, &k);
%do %while(("&old" NE "") & ("&new" NE ""));
rename &old = &new;
%let k = %eval(&k + 1);
%let old = %scan(&oldvarlist, &k);
%let new = %scan(&newvarlist, &k);
%end;
%mend;
The columns are currently named C5, C7, C9, ..., C205 and I would like to rename them AR_0, AR_1, ..., AR100.
With the macro above, how can I put these new names after the comma of the following code without writing each and every one of them?
%rename1(C5--C205, # new names here #);
This is a bit of a longer solution, but it's fairly dynamic and you easy to see how things work. I'm assuming you'll use the rename statement in proc datasets. Otherwise you could just be lazy and use arrays to replace then drop the old variables, though that isn't efficient.
proc sql;
create table oldvar as
select name, varnum
from sashelp.vcolumn
where upcase(libname)='SASHELP'
and upcase(memname)='CLASS'
order by varnum;
quit;
data rename;
set oldvar;
new_var=catx("_", "AR",varnum);
run;
proc sql noprint;
select catx("=", name, new_var) into :rename_list
separated by " "
from rename;
quit;
%put rename &rename_list;
proc datasets library=work;
modify my_dataset;
rename &rename_list;
run;quit;
This will first find the old columns and rename them to AR_# and create macrovariable varlist that you can use:
proc sql noprint;
create table newvar as
select name
from sashelp.vcolumn
where libname="SASHELP" and memname="CLASS"
order by name;
quit;
data newvar;
set newvar;
name=compress("AR_"!!put(_n_,4.));
run;
proc sql noprint;
select name into :varlist separated by " "
from newvar;
quit;
Probably, something like this would do the job
%macro rename2(oldvarlist, newPrefix);
%let k=1;
%let old = %scan(&oldvarlist, &k);
%do %while(("&old" NE ""));
rename &old = &newPrefix.&k.;
%let k = %eval(&k + 1);
%let old = %scan(&oldvarlist, &k);
%end;
%mend;

How to loop through a macro variable in SAS

I have an example like this:
proc sql;
select dealno into :deal_no
from deal_table;
Now I want to traverse the variable deal_no now containing all dealno in table deal_table but I don't know how to do it.
Another option is add 'separated by' to the sql code, which will add a delimiter to the values. You can then use the SCAN function in a data step or %SCAN in a macro to loop through the values and perform whatever task you want. Example below.
proc sql noprint;
select age into :age separated by ','
from sashelp.class;
quit;
%put &age.;
data test;
do i=1 by 1 while(scan("&age.",i) ne '');
age=scan("&age.",i);
output;
end;
drop i;
run;
If you do
%put &deal_no;
you can see that it only contains the first value of dealno, not all of them.
To avoid that you can do something like this:
proc sql;
create table counter as select dealno from deal_table;
select dealno into :deal_no_1 - :deal_no_&sqlobs
from deal_table;
quit;
%let N = &sqlobs;
%macro loop;
%do i = 1 %to &N;
%put &&deal_no_&i;
%end;
%mend;
%loop; run;
Here's another solution.
proc sql noprint;
select age into :ageVals separated by ' '
from ageData;
quit;
%put &ageVals;
%macro loopAgeVals; %let i = 1; %let ageVal = %scan(&ageVals, &i);
%do %while("&ageVal" ~= "");
%put &ageVal;
%let i = %eval(&i + 1);
%let ageVal = %scan(&ageVals, &i);
%end;
%mend;
%loopAgeVals;