SAS - Replacing substrings in a macro variable containing semicolons - variables

I need to be able to copy paste a long SQL script that contains variables back and forth between Aginity Workbench and SAS. To make this easier, I've been storing the SQL query in a macro variable like this:
(take note of the Netezza style variables)
%let myQuery = %str(
DROP TABLE this;
SELECT *
INTO SomeTable
FROM OtherTable
WHERE field = ${myVariable};
UPDATE TABLE foo
SET x = 1
WHERE
field = ${anotherVariable};
);
When my SAS program runs, I need it to replace the ${netezzaVariables} with text from other macro variables that are determined earlier in the process flow. So far, I've not been able to successfully replace text within this macro variable and I am suspicious that the semicolons are causing issues.
Here's what I'm attempting to do below:
%let formattedText = %sysfunc(tranwrd(&myQuery,'${myVariable}','replacementText'));
The log for that shows:
NOTE: Line generated by the macro function "SYSFUNC".
DROP TABLE this;
! SELECT <the rest of the query is printed to console here>
I didn't go on writing the rest of the log above, because the error is on the word SELECT, with a red line underneath it. Just below this red line is the text:
ERROR 180-322: Statement is not valid or it is used out of proper order.
In fact, if I go on scrolling through the log, every first set of characters following a semicolon in the query is underlined with that same exact error code.
This leads me to believe that SAS picked up the semicolon before that SELECT, used it to terminate what I was doing, and now thinks the text following this semicolon is out in the open code.
In conclusion, I need to know how to replace substrings within a macro variable who's value is a large string containing semicolons.
Thanks in advance!

You don't need to use quotes when using string functions with %sysfunc(). In this case %qsysfunc is what you're looking for, I think:
%let myQuery = %str(
DROP TABLE this;
SELECT *
INTO SomeTable
FROM OtherTable
WHERE field = ${myVariable};
UPDATE TABLE foo
SET x = 1
WHERE
field = ${anotherVariable};
);
%put &myQuery;
%let formattedText = %qsysfunc(tranwrd(&myQuery,${myVariable},replacementText));
%put &formattedText;

Not the most elegant of solutions but it does the job:
%let myQuery = %str(
DROP TABLE this;
SELECT *
INTO SomeTable
FROM OtherTable
WHERE field = ${myVariable};
UPDATE TABLE foo
SET x = 1
WHERE
field = ${anotherVariable};
);
data _null_;
call symput('formattedtext',tranwrd("%quote(%superq(myquery))","${myVariable}","replacementText"));
run;
%put %superq(formattedText);
There's probably a way to do it using only macro functions but I couldn't get it to work.
For your particular example, the call symput could have been simplified to
call symput('formattedtext',tranwrd("&myquery","${myVariable}","replacementText"));
but this would fail if your query contained double quotes whereas the way I wrote it above supports that.

Joshua:
A general purpose resolver is useful if your 'templated' expression has many parameters. Note: a template is different than a parameterized query, and potentially more dangerous.
Without a resolver you will need to code a TRANWRD for each parameter.
Consider this macro that assumes a template contains parameters that are specified by ${macro-var} and parameters are replaced with the macro-var value. Also presume there are no parameters that start with underscore (_) which could collide with the macros internal variables.
%macro resolver(_template);
%local _result;
%local _tokenRx;
%local _start _stop _position _length _token _macrovar _guard;
%let _tokenRx = %sysfunc(prxparse(m/\${([^}]+)}/));
/*%put &=_tokenRx;*/
%let _guard = 0;
%let _start = 1;
%let _stop = %length(&_template);
%let _position = 0;
%let _length = 0;
%let _result = &_template;
%syscall prxnext(_tokenRx, _start, _stop, _template, _position, _length);
%do %while (&_position > 0);
/* %put &=_start &=_stop &=_position &=_length; */
%let _token = %qsubstr(&_template,&_position,&_length);
%let _macrovar = %substr(&_token,3,%eval(%length(&_token)-3));
/*
%put &=_token;
%put &=_macrovar;
*/
%if %symexist(&_macrovar) %then %do;
%let _result = %qsysfunc(tranwrd(&_result,&_token,&&&_macrovar));
%end;
%syscall prxnext(_tokenRx, _start, _stop, _template, _position, _length);
%let _guard = %eval (&_guard+1);
%if &_guard > 1000 %then %let _position = 0;
%end;
%syscall prxfree(_tokenRx);
%superq(_result)
%mend;
Here is the resolver applied to your templated SQL query (adjusted to Proc SQL).
%let myQuery = %str(
DROP TABLE this
;
INSERT INTO SomeTable /* sas insert syntax */
SELECT * FROM OtherTable
WHERE ${field} = ${target}
;
UPDATE foo
SET x = 1
WHERE
field = ${anotherVariable}
;
);
%let field = name;
%let target = 'Jane';
%let myVariable = XYZ;
%let anotherVariable = 'John';
%put %resolver (%superq(myQuery));
proc sql;
create table this (id int);
create table SomeTable like sashelp.class;
create table OtherTable as select * from sashelp.class;
create table foo as select name as field, 0 as x from sashelp.class;
%unquote(%resolver(%superq(myQUery)))
quit;

Related

Conditional PROC SQL on SAS

I have a SAS Script containing multiple PROC SQLs. The question is that the SQL Query should be "adapted" based on a SAS variable value, for example :
%let COD_COC =(52624, 52568, 52572);
%let COD_BLOC = ();
proc sql;
create table work.abordados as
select t1.cd_acao,
t1.cd_bloc,
t1.cd_tip_cnl,
t1.cd_cmco,
t1.cd_cli,
datepart(t1.ts_ctt) format=date9. as data_abordagem,
intnx('day',datepart(t1.ts_ctt), &Prazo_Compra) format=date9. as data_limite
from db2coc.ctt_cli_cmco t1
where (t1.cd_acao in &COD_COC)
and (t1.cd_bloc in &COD_BLOC) <<<<<<< facultative filter
;quit;
The question is that the second filter (t1.cd_bloc in &COD_BLOC) should be applied only if the %let COD_BLOC = (); is different of "()".
I´ve been reading about "match/case" on SQL but as far as I know, this test applies to results of queries/values. On my case, what I must test is the SAS variable.
How handle this?
Knowing you want to apply the COD_BLOC in-list filter only when there are one or more values, AND that a proper in-list will have at least 3 source code characters (*), you can test the length as the criteria for using the macro variable.
When the %IF is in open code you need a %do %end block as follows:
...
%if %length(&COD_BLOC) > 2 %then %do;
and t1.cd in &COD_BLOC
%end;
...
When the code is inside a macro, you can use the above or the below
...
%if %length(&COD_BLOC) > 2 %then
and t1.cd in &COD_BLOC
;
...
Another possible coding solution is to use %sysfunc(IFC(...)) to conditionally generate code
...
%sysfunc(ifc(%length(&COD_BLOC) > 2
, and t1.cd in &COD_BLOC
, %str()
))
...
Two good ways to do this, I think.
First: the easy hack.
%let COD_COC =(52624, 52568, 52572);
%let COD_BLOC = (and t1.cd_bloc in (...));
%let COD_BLOC = ;
proc sql;
create table work.abordados as
select t1.cd_acao,
t1.cd_bloc,
t1.cd_tip_cnl,
t1.cd_cmco,
t1.cd_cli,
datepart(t1.ts_ctt) format=date9. as data_abordagem,
intnx('day',datepart(t1.ts_ctt), &Prazo_Compra) format=date9. as data_limite
from db2coc.ctt_cli_cmco t1
where (t1.cd_acao in &COD_COC)
&COD_BLOC <<<<<<< facultative filter
;quit;
Tada, now it is just ignored. Comment out or delete the second line if you want it to be used (and put values in the ... ).
Second, the more proper way, is to use the macro language. This is more commonly done in a macro, but in 9.4m7 (the newest release, and a few years old now) you can do this in open code.
%let COD_COC =(52624, 52568, 52572);
%let COD_BLOC = ();
proc sql;
create table work.abordados as
select t1.cd_acao,
t1.cd_bloc,
t1.cd_tip_cnl,
t1.cd_cmco,
t1.cd_cli,
datepart(t1.ts_ctt) format=date9. as data_abordagem,
intnx('day',datepart(t1.ts_ctt), &Prazo_Compra) format=date9. as data_limite
from db2coc.ctt_cli_cmco t1
where (t1.cd_acao in &COD_COC)
%if %sysevalf(%superq(COD_BLOC) ne %nrstr(%(%)),boolean) %then %do;
and (t1.cd_bloc in &COD_BLOC) <<<<<<< facultative filter
%end;
;quit;
You have to be careful with the ne () bit because () are macro language syntax elements, hence the long %nrstr to make sure they're properly considered characters. (%str would be okay too, I just default to %nrstr.)

Using SQL to pass lists of values into macro variables in SAS

I have blocks of variables that I need to process. I put a list of the variable block names into a macro variable. Then I created a list of macro variables I want to pass the variable lists into. I am attempting to iterate through the list with this code but the only thing that is retained a macro variable called nextlist for the last block of variables. How can I get the SQL code to put the list of values into the macros I am feeding in from the &blockvarlist macro list?
I am running this in SAS 9.4
PROC SQL ; SELECT DISTINCT FINAL_NAME INTO :&next SEPARATED BY " " FROM
metadata4b
where varblockname = "&next2" and type = "Num" and
(index(final_name,"_NA") = 0 and index(final_name,"_1NA")
= 0 and index(final_name,"_2NA") = 0 ); QUIT;
%put &blocklist;
/*result of put: Q11 Q11_10N Q11_11N Q11_12N Q38_list*/
%put &blockvarlist;
/*result of put: Q11_10N_list Q11_11N_list Q11_12N_list Q38_list*/
%macro createarray;
%global i nextblock;
%do i = 1 %to %sysfunc(countw(&BLOCKLIST));
%let nextblock = %scan(&BLOCKLIST, &i, %str( ));
%let nextlist = %scan(&BLOCKVARLIST, &i, %str( ));
PROC SQL; SELECT DISTINCT FINAL_NAME INTO :nextlist SEPARATED BY
" " FROM metadata4b
where varblockname = "&nextblock" and type = "Num" and
(index(final_name,"_NA") = 0 and index(final_name,"_1NA")
= 0 and index(final_name,"_2NA") = 0 ); QUIT;
%end;
%mend;
%createarray;
I want to generate a series of macro variables that I can reference later. For example Q38_list would contain the list of variables in that question block: Q38_CIR Q38_FRM Q38_OTR Q38_SCR
I think you may need to resolve the nextlist macro variable during the INTO.
You might also need to globalize the target prior to the SQL
%let nextlist = %scan(&BLOCKVARLIST, &i, %str( ));
%global &nextlist;
Change
SELECT DISTINCT FINAL_NAME INTO :nextlist
to
SELECT DISTINCT FINAL_NAME INTO :&nextlist

How to handle multiple filters in SAS stored process

I'm creating a SAS stored process report in which user can filter data using different cafeterias.
This is what I'm doing
%let ID_WHERE_CLAUSE=;
%let Source = "group";
%let ActionRequired = "Daily";
%macro SetFilters;
data _null_;
%if &&ActionRequired ne "A" %then %do;
%let test = "ActionR";
call symputx('ID_WHERE_CLAUSE',cats(' and ActionRequired = ',' &ActionRequired',''));
%end;
%if &&Source ne "A" %then %do;
%let test = "Source";
call symputx('ID_WHERE_CLAUSE',cats('and Source = ',' &Source',''));
%end;
run;
%mend; %SetFilters;
%put &ID_WHERE_CLAUSE;
in ID_WHERE_CLAUSE data should be as and action required = "Daily" and source = "group" but my code is only appending last filter to my ID_WHERE_CLAUSE variable as and source = "group"
my expected result is and action required = "Daily" and source = "group"
but actual result I'm getting is and source = "group"
That's why I'm not getting my expected result. How can I append all where conditions in my ID_WHERE_CLAUSE variable.
I tried this too but didn't work either.
call symputx('ID_WHERE_CLAUSE',' and ActionRequired = &ActionRequired','');
call symputx('ID_WHERE_CLAUSE', ' and Source = &Source');
I'm using it as
proc sql noprint;
create table filter_data as
select * from data
where 1=1 &ID_WHERE_CLAUSE;
quit;
Kindly tell me how can i concatenate all where clause to it or any better approach.
call symputx does not append to macro variables - it overwrites them. So you need to capture the existing value as part of the new value you want to set. E.g.
call symputx('ID_WHERE_CLAUSE',cats(symget("ID_WHERE_CLAUSE"),'and Source = ',' &Source',''));
You need to use symget to do this in your scenario rather than referencing the macro variable directly, otherwise it may be resolved before your earlier data step executes and sets the initial value.

Get Data from SAS Macro (list of values) to SAS table (column)

I am trying to create a SAS table from Macro variable using PROC SQL:
I have a list of value saved in a macro variable :
%let l=1,2,3;
I want to create a SAS table with a column containing the values of the macro variable :
1
2
3
Thank you very much for your help.
Sincerely,
Abdeljalil
you should so some effort to solving this yourself.
Put the values into a string, parse the string and output the values you would like.
%let l=1,2,3;
data want;
str = "&l";
do i=1 to countw(str,',');
value = input(scan(str,i,","),best.);
output;
end;
/*drop other variables if you want*/
drop str i;
run;
Something like this?
%let age=%str(12,13,15);
proc sql;
select * from sashelp.class where age in (&age);
quit;
You have a data set that contains a list of names and you want to place these names into a macro variable for later use. That will work as long as the macro variable does not go beyond the 64K limit.
If the value hits this limit, then you can use macro processing to retrieve the names from the data set. Since a macro definition does not have the 64K restriction, it can be used to create the list for you.
In the sample code on the Full Code tab, we have a list of names that we want to use on an INPUT statement along with a given informat. This sample demonstrates how to create the list without having to use a macro variable.
data one;
input name $;
datalines;
abc
def
ghi
;
run;
%macro test;
%let dsid=%sysfunc(open(one));
%let cnt=%sysfunc(attrn(&dsid,nobs));
%do i=1 %to &cnt;
%let rc=%sysfunc(fetchobs(&dsid,&i));
%cmpres(%sysfunc(getvarc(&dsid,%sysfunc(varnum(&dsid,name))))) $4.
%end;
%let rc=%sysfunc(close(&dsid));
%mend test;
/** Using %PUT to see outcome **/
/** %test could be used on an INPUT statement **/
%put %test;
source: http://support.sas.com/kb/39/605.html

Select character variables that have all missing values

I have a SAS dataset with around 3,000 variables, and I would like to get rid of the character variables for which all values are missing. I know how to do this for numeric variables-- I'm wondering specifically about the character variables. I need to do the work using base SAS, but that could include proc SQL, which is why I've tagged this one 'SQL' also.
Thank you!
Edit:
Background info: This is a tall dataset, with survey data from 7 waves of interviews. Some, but not all, of the survey items (variables) were repeated across waves. I'm trying to create a list of items that were actually used in each wave by pulling all the records for that wave, getting rid of all the columns that have nothing but SAS's default missing values, and then running proc contents.
I created a macro that will check for empty character columns and either remove them from the original or create a new data set with the empty columns removed. It takes two optional arguments: The name of the data set (default is the most recently created data set), and a suffix to name the new copy (set suffix to nothing to edit the original).
It uses proc freq with the levels option and a custom format to determine the empty character columns. proc sql is then used to create a list of the columns to be removed and store them in a macro variable.
Here is the macro:
%macro delemptycol(ds=_last_, suffix=_noempty);
option nonotes;
proc format;
value $charmiss
' '= ' '
other='1';
run;
%if "&ds"="_last_" %then %let ds=&syslast.;
ods select nlevels;
ods output nlevels=nlev;
proc freq data=&ds.(keep=_character_) levels ;
format _character_ $charmiss.;
run;
ods output close;
/* create macro var with list of cols to remove */
%local emptycols;
proc sql noprint;
select tablevar into: emptycols separated by ' '
from nlev
where NNonMissLevels=0;
quit;
%if &emptycols.= %then %do;
%put DELEMPTYCOL: No empty character columns were found in data set &ds.;
%end;
%else %do;
%put DELEMPTYCOL: The following empty character columns were found in data set &ds. : &emptycols.;
%put DELEMPTYCOL: Data set &ds.&suffix created with empty columns removed;
data &ds.&suffix. ;
set &ds(drop=&emptycols);
run;
%end;
options notes;
%mend;
Examples usage:
/* create some fake data: Here char5 will be empty */
data chardata(drop= j randnum);
length char1-char5 $8.;
array chars(5) char1-char5;
do i=1 to 100;
call missing(of char:);
randnum=floor(10*ranuni(i));
do j=2 to 5;
if (j-1)<randnum<=(j+1) then chars(j-1)="FOO";
end;
output;
end;
run;
%delemptycol(); /* uses default _last_ for the data and "_noempty" as the suffix */
%delemptycol(ds=chardata, suffix=); /* removes the empty columns from the original */
There's probably a simpler way but this is what I came up with.
Cheers
Rob
EDIT: Note that this works for both character and numeric variables.
**
** TEST DATASET
*;
data x;
col1 = "a"; col2 = ""; col3 = "c"; output;
col1 = "" ; col2 = ""; col3 = "c"; output;
col1 = "a"; col2 = ""; col3 = "" ; output;
run;
**
** GET A LIST OF VARIABLE NAMES
*;
proc sql noprint;
select name into :varlist separated by " "
from sashelp.vcolumn
where upcase(libname) eq "WORK"
and upcase(memname) eq "X";
quit;
%put &varlist;
**
** USE A MACRO TO CREATE A DATASTEP. FOR EACH COLUMN THE
** THE DATASTEP WILL CREATE A NEW COLUMN WITH THE SAME NAME
** BUT PREFIXED WITH "DELETE_". IF THERE IS AT LEAST 1
** NON-MISSING VALUE FOR THE COLUMN THEN THE "DELETE" COLUMN
** WILL FINISH WITH A VALUE OF 0, ELSE 1. WE WILL ONLY
** KEEP THE COLUMNS CALLED "DELETE_" AND OUTPUT ONLY A SINGLE
** OBSERVATION TO THE FINAL DATASET.
*;
%macro find_unused_cols(iDs=);
%local cnt;
data vars_to_delete;
set &iDs end=eof;
%let cnt = 1;
%let varname = %scan(&varlist, &cnt);
%do %while ("&varname" ne "");
retain delete_&varname;
delete_&varname = min(delete_&varname, missing(&varname));
drop &varname;
%let cnt = %eval(&cnt + 1);
%let varname = %scan(&varlist, &cnt);
%end;
if eof then do;
output;
end;
run;
%mend;
%find_unused_cols(iDs=x);
**
** GET A LIST OF VARIABLE NAMES FROM THE NEW DATASET
** THAT WE WANT TO DELETE AND STORE TO A MACRO VAR.
*;
proc transpose data=vars_to_delete out=vars_to_delete;
run;
proc sql noprint;
select substr(_name_,8) into :vars_to_delete separated by " "
from vars_to_delete
where col1;
quit;
%put &vars_to_delete;
**
** CREATE A NEW DATASET CONTAINING JUST THOSE VARS
** THAT WE WANT TO KEEP
*;
data new_x;
set x;
drop &vars_to_delete;
run;
Rob and cmjohns, thank you SO MUCH for your help. Based on your solutions and an idea I had over the weekend, here is what I came up with:
%macro removeEmptyCols(origDset, outDset);
* get the number of obs in the original dset;
%let dsid = %sysfunc(open(&origDset));
%let origN = %sysfunc(attrn(&dsid, nlobs));
%let rc = %sysfunc(close(&dsid));
proc transpose data= &origDset out= transpDset;
var _all_;
run;
data transpDset;
set transpDset;
* proc transpose converted all old vars to character,
so the . from old numeric vars no longer means 'missing';
array oldVar_ _character_;
do over oldVar_;
if strip(oldVar_) = "." then oldVar_ = "";
end;
* each row from the old dset is now a column with varname starting with 'col';
numMiss = cmiss(of col:);
numCols = &origN;
run;
proc sql noprint;
select _NAME_ into: varsToKeep separated by ' '
from transpDset
where numMiss < numCols;
quit;
data &outDset;
set &origDset (keep = &varsToKeep);
run;
%mend removeEmptyCols;
I will try all 3 ways and report back on which one is fastest...
P.S. added 23 Dec 2010 for future reference: SGF Paper 048-2010: Dropping Automatically Variables with Only Missing Values
This is very simple method useful for all variables
proc freq data=class nlevels ;
ods output nlevels=levels(where=(nmisslevels>0 and nnonmisslevels=0));
run;
proc sql noprint;
select TABLEVAR into :_MISSINGVARS separated by ' ' from levels;
quit;
data want;
set class (keep=&_MISSINGVARS);
run;