How to access nth element of array within loop in macro SAS? - sas

I'm trying to get the ith item of an array within %do loop in macro definition and create a dataset with the name of the element, but all I can get is something like "z1" etc. This is what I got so far
%macro print(set,groupvar);
proc sql ;
select put(count(distinct &groupvar),1.)
into :hm
from &set
;
select distinct set
into :z1-:z&sysmaxlong
from &set
;
quit;
data %do i =1 %to &hm;
%scan(&z, &i);
%end;
;
%mend;
I also tried z[&i] instead of %scan(&z,&i) but still no luck

Issues:
You have a macro variable SET but you have the distinct SET in the select query, is that what you intended?
select distinct set
You don't need to specify the end of a series when creating macro variables.
select distinct &set into :z1- from &set; quit;
You created a series of macro variables Z1-Zn but try and use SCAN to retrieve the values? They're not stored in an array, its stored in a series of macro variables such as Z1, Z2 etc.

An output statement can not specify a dynamic destination table. You will need to create wallpaper code to output to the appropriate table based on splitting criteria.
Your macro will need to create macro variables to support this code template
data &out1 &out2 … &outN;
set input_data;
select;
when (&case1) output &out1;
when (&case2) output &out2;
…
when (&caseN) output &outN;
otherwise;
end;
run;
Some slick SQL could support the template
data &outlist;
set input_data;
select;
&whenStatements;
otherwise;
end;
run;
The hash object .output() method can specify a dynamic destination for saving hash contents. Consider the case of "Split a table into subset tables whose names are based on a variables values" aka "Split one data set into several data sets named according to a group variable". Some methods require a pre-parse of the data (as in your posted code), others do not.
There is some hash based splitter code at https://www.devenezia.com/downloads/sas/samples/hash-6.sas
You can find other data splitters on sas community.

If you want to use %scan() then just create one macro variable.
select distinct set
into :z separated by ' '
from &set
;
%let hm=&sqlobs;
The DATA statement will end at the first semi-colon. But you are generating &hm+1 semi-colons instead of just one. Remove the spurious semi-colon from inside the %do loop.
data
%do i =1 %to &hm;
%scan(&z, &i)
%end;
;

Related

SAS. how to define an array using a list of variables stored in a macro variable

I have a list of 15 similar variables that I want to loop through syntax to recode as null. I will be adding more variables to this list so that's why I went with a macro. I created a macro variable that stores a list of variables and another macro variable that stores the count of variables. I am having trouble defining the array. I got the code below to work, but it only goes through varlist1, and not the other 14 variables (varlist2-varlist15)... which is what I told it to do but I can't figure out how to expand it to the full list of variables without breaking the code. I searched through the forums and SAS articles but couldn't find an answer. I'm fairly new to arrays so I'm sure it's something simple I don't understand. appreciate any help and let me know if I can post better next time. first one. :)
this is my list in &varlist.
(
NEW_CREWSTATE1A
NEW_EquFailText
NEW_ExpMedNotList
NEW_NARRATIVEDISTRESS
NEW_NarrativeRecovery
NEW_NarrativeSearch
NEW_OthEquText
NEW_PersReqMedTxt
NEW_ProbEnct
NEW_Recommend
NEW_RescueSwimProbText
NEW_RescuerProb
NEW_SRUConfigTxt
NEW_equimalissue
new_MedDiffText
)
code:
proc sql noprint;
select count(name) into :numVar
from sashelp.vcolumn
where upcase(LIBname)="STAGING" and UPCASE(memname)="RESC_NARRATIVEMERGE" and UPCASE(name) like 'NEW_%';
quit;
%put &numVar;
proc sql noprint;
select distinct(name) into :varlist1-
from sashelp.vcolumn
where upcase(LIBname)="STAGING" and UPCASE(memname)="RESC_NARRATIVEMERGE" and UPCASE(name) like 'NEW_%';
quit;
data staging.RESC_NARRATIVEMERGE2;
set staging.RESC_NARRATIVEMERGE;
array narrative_array {*} &varlist1. ;
do i=1 to dim(narrative_array);
if strip(narrative_array{i})='N/A' then narrative_array{i}='';
if strip(narrative_array{i})='N/A.' then narrative_array{i}='';
if strip(narrative_array{i})='NA' then narrative_array{i}='';
if strip(narrative_array{i})='NONE' then narrative_array{i}='';
if strip(narrative_array{i})='NONE NOTED' then narrative_array{i}='';
if strip(narrative_array{i})='NONE EXPERIENCED' then narrative_array{i}='';
if strip(narrative_array{i})='NONE TO REPORT' then narrative_array{i}='';
if strip(narrative_array{i})='NONE.' then narrative_array{i}='';
if strip(narrative_array{i})='NOT APPLICABLE' then narrative_array{i}='';
end;
run;
The only thing you need to define an array is the actual list of variables names.
array narrative_array Avar Anothervar Someothervar ;
So just put the list of variable names into ONE macro variable.
proc sql noprint;
select nliteral(name)
into :varlist separated by ' '
from dictionary.columns
where libname="STAGING"
and memname="RESC_NARRATIVEMERGE"
and UPCASE(name) like 'NEW^_%' escape '^'
;
quit;
Note that there is no need to count them, but if you want the count then SQL will have already stored the count into the macro variable SQLOBS. Perhaps you can use the count to decide whether or not you need to define the array at all.
data staging.RESC_NARRATIVEMERGE2;
set staging.RESC_NARRATIVEMERGE;
%if &sqlobs %then %do;
array narrative_array &varlist ;
do index=1 to dim(narrative_array);
if left(compbl(narrative_array[index])) in
('N/A','N/A.','NA','NONE','NONE NOTED','NONE EXPERIENCED'
,'NONE TO REPORT','NONE.','NOT APPLICABLE')
then narrative_array[index]=' ';
end;
drop index ;
%end;
run;
Look at your variable name selection criteria
where upcase(LIBname)="STAGING"
and UPCASE(memname)="RESC_NARRATIVEMERGE"
and UPCASE(name) like 'NEW_%'
;
You are looking for variable names that start with NEW_. The DATA Step has a naming list syntax that selects variables based on prefix (<prefix>:), and that list can be used to specify the members of an array.
If there are no variables that match the specified list the array will have zero elements and the log will show the message "WARNING: Defining an array with zero elements." A loop can be coded simply as 1 to DIM(<array-name>) and no iterations will occur because the DIM() result is 0
data ...
set STAGING.RESC_NARRATIVEMERGE;
array NEW NEW_:;
As shown by #Tom your wallpaper of tests to transform non-values to blanks can be changed to a IN list for better performance and human clarity.

Create a macro that applies translate on multiple columns that you define in a dataset

I'm new to programming in SAS and I would like to do 2 macros, the first one I have done and it consists of giving 3 parameters: name of the input table, name of the column, name of the output table. What this macro does is translate the rare or accented characters, passing it a table and specifying in which column you want the rare characters to be translated:
The code to do this macro is this:
%macro translate_column(table,column,name_output);
*%LET table = TEST_MACRO_TRNSLT;
*%let column = marca;
*%let name_output = COSAS;
PROC SQL;
CREATE TABLE TEST AS
SELECT *
FROM &table.;
QUIT;
data &NAME_OUTPUT;
set TEST;
&column.=tranwrd(&column., "Á", "A");
run;
%mend;
%translate_column(TEST_MACRO_TRNSLT,marca,COSAS);
The problem comes when I try to do the second macro, that I want to replicate what I do in the first one but instead of having the columns that I can introduce to 1, let it be infinite, that is, if in a data set I have 4 columns with characters rare, can you translate the rare characters of those 4 columns. I don't know if I have to put a previously made macro in a parameter and then make a kind of loop or something in the macro.
The same by creating a kind of array (I have no experience with this) and putting those values in a list (these would be the different columns you want to iterate over) or in a macrovariable, it may be that passing this list as a function parameter works.
Could someone give me a hand on this? I would be very grateful
Either use an ARRAY or a %DO loop.
In either case use a space delimited list of variable names as the value of the COLUMN input parameter to your macro.
%translate_column
(table=TEST_MACRO_TRNSLT
,column=var1 varA var2 varB
,name_output=COSAS
);
So here is ARRAY based version:
%macro translate_column(table,column,name_output);
data &NAME_OUTPUT;
set &table.;
array __column &column ;
do over __column;
__column=ktranslate(__column, "A", "Á");
end;
run;
%mend;
Here is %DO loop based version
%macro translate_column(table,column,name_output);
%local index name ;
data &NAME_OUTPUT;
set &table.;
%do index=1 %to %sysfunc(countw(&column,%str( )));
%let name=%scan(&column,&index,%str( ));
&name = ktranslate(&name, "A", "Á");
%end;
run;
%mend;
Notice I switched to using KTRANSLATE() instead of TRANWRD. That means you could adjust the macro to handle multiple character replacements at once
&name = ktranslate(&name,'AO','ÁÓ');
The advantage of the ARRAY version is you could do it without having to create a macro. The advantage of the %DO loop version is that it does not require that you find a name to use for the array that does not conflict with any existing variable name in the dataset.

SAS Rename variables using a list of variables in a macro

A novice in SAS here. I am trying to rename variables in a data set by using the new values I have in a list. Since I have multiple files with over 100 variables that need to be renamed, I created the following macro and I am trying to pass the list with the new names. However, I am not sure how to pass the list of variables and loop through it properly in the macro. Right now I am getting an error in the %do loop that says: "ERROR: The %TO value of the %DO I loop is invalid."
Any guidance will be greatly appreciated.
The list of new variables comes from another macro and it is saved in &newvars.
The number of variables in the files are the same number in the list, and the order they should be replaced is the same.
%macro rename(lib,dsn,newname);
proc sql noprint;
select nvar into :num_vars from dictionary.tables
where libname="&LIB" and memname="&DSN";
select distinct(nliteral(name)) into:vars
from dictionary.columns
where libname="&LIB" and memname="&DSN";
quit;
run;
proc datasets library = &LIB;
modify &DSN;
rename
%do i = 1 %to &num_vars.;
&&vars&i == &&newname&i.
%end;
;
quit;
run;
%mend rename;
%rename(pga3,selRound,&newvars);
Thank you in advance.
You are getting that error message because the macro variable NUM_VARS is not being set because no observations met your first where condition.
The LIBNAME and MEMNAME fields in the metadata tables are always uppercase and you called the macro with lowercase names.
You can use the %upcase() macro function to fix that. While you are at it you can eliminate the first query as SQL will count the number of variables for you in the second query. Also if you want that query to generate multiple macro variables with numeric suffixes you need to modify the into clause to say that. The DISTINCT keyword is not needed as a dataset cannot have two variables with the same name.
select nliteral(name)
into :vars1 -
from dictionary.columns
where libname=%upcase("&LIB") and memname=%upcase("&DSN")
;
%let num_vars=&sqlobs;
You also should tell it what order to generate the names. Were the new names generated in expectation that the list would be in the order the variables exist in the dataset? If so use the VARNUM variable in the ORDER BY clause. If in alphabetical order then use NAME in the ORDER BY clause.
How are you passing in the new names?
Is it a space delimited list? If so your final step should look more like this:
proc datasets library = &LIB;
modify &DSN;
rename
%do i = 1 %to &num_vars.;
&&vars&i = %scan(&newname,&i,%str( ))
%end;
;
run; quit;
If NEWNAME has the base name to use for a series of variable names with numeric suffixes then you would want this:
&&vars&i = &newname&i
If instead you are passing into NEWNAME a base string to use to locate a series of macro variables with numeric suffixes then the syntax would be more like this.
&&vars&i = &&&newname&i
So if NEWNAME=XXX and I=1 then on the first pass of the macro processor that line will transform into
&vars1 = &XXX1
And on the second pass the values of VARS1 and XXX1 would be substituted.

Using SAS SET statement with numbered macro variables

I'm trying to create a custom transformation within SAS DI Studio to do some complicated processing which I will want to reuse often. In order to achieve this, as a first step, I am trying to replicate the functionality of a simple APPEND transformation.
To this end, I've enabled multiple inputs (max of 10) and am trying to leverage the &_INPUTn and &_INPUT_count macro variables referenced here. I would like to simply use the code
data work.APPEND_DATA / view=work.APPEND_DATA;
%let max_input_index = %sysevalf(&_INPUT_count - 1,int);
set &_INPUT0 - &&_INPUT&max_input_index;
keep col1 col2 col3;
run;
However, I receive the following error:
ERROR: Missing numeric suffix on a numbered data set list (WORK.SOME_INPUT_TABLE-WORK.ANOTHER_INPUT_TABLE)
because the macro variables are resolved to the names of the datasets they refer to, whose names do not conform to the format required for the
SET dataset1 - dataset9;
statement. How can I get around this?
Much gratitude.
You need to create a macro that loops through your list and resolves the variables. Something like
%macro list_tables(n);
%do i=1 %to &n;
&&_INPUT&i
%end;
%mend;
data work.APPEND_DATA / view=work.APPEND_DATA;
%let max_input_index = %sysevalf(&_INPUT_count - 1,int);
set %list_tables(&max_input_index);
keep col1 col2 col3;
run;
The SET statement will need a list of the actual dataset names since they might not form a sequence of numeric suffixed names.
You could use a macro %DO loop if are already running a macro. Make sure to not generate any semi-colons inside the %DO loop.
set
%do i=1 %to &_inputcount ; &&_input&i %end;
;
But you could also use a data step to concatenate the names into a single macro variable that you could then use in the SET statement.
data _null_;
call symputx('_input1',symget('_input'));
length str $500 ;
do i=1 to &_inputcount;
str=catx(' ',str,symget(cats('_input',i)));
end;
call symputx('_input',str);
run;
data .... ;
set &_input ;
...
The extra CALL SYMPUTX() at the top of the data step will handle the case when count is one and SAS only creates the _INPUT macro variable instead of creating the series of macro variables with the numeric suffix. This will set _INPUT1 to the value of _INPUT so that the DO loop will still function.

SAS - Creating variables from macro variables

I have a SAS dataset which has 20 character variables, all of which are names (e.g. Adam, Bob, Cathy etc..)
I would like a dynamic code to create variables called Adam_ref, Bob_ref etc.. which will work even if there a different dataset with different names (i.e. don't want to manually define each variable).
So far my approach has been to use proc contents to get all variable names and then use a macro to create macro variables Adam_ref, Bob_ref etc..
How do I create actual variables within the dataset from here? Do I need a different approach?
proc contents data=work.names
out=contents noprint;
run;
proc sort data = contents; by varnum; run;
data contents1;
set contents;
Name_Ref = compress(Name||"_Ref");
call symput (NAME, NAME_Ref);
%put _user_;
run;
If you want to create an empty dataset that has variables named like some values you have in a macro variables you could do something like this.
Save the values into macro variables that are named by some pattern, like v1, v2 ...
proc sql;
select compress(Name||"_Ref") into :v1-:v20 from contents;
quit;
If you don't know how many values there are, you have to count them first, I assumed there are only 20 of them.
Then, if all your variables are character variables of length 100, you create a dataset like this:
%macro create_dataset;
data want;
length %do i=1 %to 20; &&v&i $100 %end;
;
stop;
run;
%mend;
%create_dataset; run;
This is how you can do it if you have the values in macro variable, there is probably a better way to do it in general.
If you don't want to create an empty dataset but only change the variable names, you can do it like this:
proc sql;
select name into :v1-:v20 from contents;
quit;
%macro rename_dataset;
data new_names;
set have(rename=(%do i=1 %to 20; &&v&i = &&v&i.._ref %end;));
run;
%mend;
%rename_dataset; run;
You can use PROC TRANSPOSE with an ID statement.
This step creates an example dataset:
data names;
harry="sally";
dick="gordon";
joe="schmoe";
run;
This step is essentially a copy of your step above that produces a dataset of column names. I will reuse the dataset namerefs throughout.
proc contents data=names out=namerefs noprint;
run;
This step adds the "_Refs" to the names defined before and drops everything else. The variable "name" comes from the column attributes of the dataset output by PROC CONTENTS.
data namerefs;
set namerefs (keep=name);
name=compress(name||"_Ref");
run;
This step produces an empty dataset with the desired columns. The variable "name" is again obtained by looking at column attributes. You might get a harmless warning in the GUI if you try to view the dataset, but you can otherwise use it as you wish and you can confirm that it has the desired output.
proc transpose out=namerefs(drop=_name_) data=namerefs;
id name;
run;
Here is another approach which requires less coding. It does not require running proc contents, does not require knowing the number of variables, nor creating a macro function. It also can be extended to do some additional things.
Step 1 is to use built-in dictionary views to get the desired variable names. The appropriate view for this is dictionary.columns, which has alias of sashelp.vcolumn. The dictionary libref can be used only in proc sql, while th sashelp alias can be used anywhere. I tend to use sashelp alias since I work in windows with DMS and can always interactively view the sashelp library.
proc sql;
select compress(Name||"_Ref") into :name_list
separated by ' '
from sashelp.vcolumn
where libname = 'WORK'
and memname = 'NAMES';
quit;
This produces a space delimited macro vaiable with the desired names.
Step 2 To build the empty data set then this code will work:
Data New ;
length &name_list ;
run ;
You can avoid assuming lengths or create populated dataset with new variable names by using a slightly more complicated select statement.
For example
select compress(Name)||"_Ref $")||compress(put(length,best.))
into :name_list
separated by ' '
will generate a macro variable which retains the previous length for each variable. This will work with no changes to step 2 above.
To create populated data set for use with rename dataset option, replace the select statement as follows:
select compress(Name)||"= "||compress(_Ref")
into :name_list
separated by ' '
Then replace the Step 2 code with the following:
Data New ;
set names (rename = ( &name_list)) ;
run ;