Creating a temporary SAS array of dynamic size

Arrays in SAS are incredibly useful things. For example, if you’re dealing with a concomitant medications dataset and want to check for certain medicines across multiple columns, you’d be hard-pressed to find a faster method than using arrays!

Within the SDTM.CM domain, medication names are spread across several columns, usually: CMTRT (Reported Name of Drug, Med, or Therapy), CMMODIFY (Modified Reported Name), and CMDECOD (Standardized Medication Name).

If we wanted to find and flag, for example, the following three medicines: Aspirin, Antacid, Potassium Chloride ; we could do it as follows:


data cm;
    set sdtm.cm;

    array all_meds {*} $ cmtrt cmmodify cmdecod;
    array check_meds {3} $ _temporary_ ("aspirin", "antacid", "potassium chloride");

    do i = 1 to dim(all_meds);
        do j = 1 to dim(check_meds);
            if upcase(all_meds[i]) = upcase(check_meds[j]) then occur = "Y";
        end;
    end;

    if occur ^= "Y" then delete;
run;

The above method works well to quickly check for multiple conmeds across multiple columns. However, usually a conmeds list is not restricted to only 3 items and counting the number of unique items is a slow and tedious process. If an item needs to be added or removed, then the space to be reserved needs to be updated.

Unfortunately specifying the list of conmeds in a temporary array prevents the use of the dynamic sizing, usually indicated by {*}. This is something that will hopefully be fixed in a newer version of SAS, but until such time I’ve taken it upon myself to create a macro which creates dynamically sized (sort of!) temporary arrays. It does this by counting the number of items and automatically reserving the space for it.

Note: if you are feeling lazy, you can still create a temporary array and simply oversize it, e.g., specify a size of 100 items even though you may only need half that. However, this will result in SAS posting a WARNING to the log about partial array initialization, which is not ideal in the pharmaceutical environment where we want clean logs!

If you’d prefer to avoid another O(n) loop which checks for the maximum length needed for character variables, simply set a static length, say $20.


/*
    Macro makeTempArray
    
    Purpose: To create temporary arrays without knowing the
             size of the array needed beforehand. This is a 
             limitation of the original SAS procedure for
             creating temporary arrays.
    
    Parameters:
        arrayname : an arbitrary name for your array
        ischar    : pass either Y for a character array 
                    or N for numeric
        items     : pass the list of items to be contained in
                    the array, wrapped in %str() and separated
                    by commas
*/

%macro makeTempArray(arrayname=, ischar=, items=);
    %let n=%sysfunc(countw(&items., %str(,), )); /*count the number of items to reserve space for*/
   
    %if &ischar.=Y %then %do; /*if this is a character array, we need the length of the longest item*/ 
        %let l = 1;
        %do j = 1 %to &n.;
            %let l0 = %sysfunc(length(%sysfunc(scan(&items., &j., %str(,), r))));
            %if  &l0. > &l. %then %let l = &l0.;
        %end;
    %end;
    
    array &arrayname. {&n.} %if &ischar.=Y %then $&l.; _temporary_ (
        %do i = 1 %to &n.;
            %let item = %sysfunc(scan(&items., &i., %str(,), r));
            %if &ischar.=Y %then %str("&item." ); %else &item.;
        %end;
    );
%mend makeTempArray;

With this macro, we can now modify our initial starting block of code as follows (items must be separated by commas):


data cm;
    set sdtm.cm;

    array all_meds {*} $ cmtrt cmmodify cmdecod;
    %makeTempArray(arrayname=%str(check_meds), ischar=%str(Y), items=%str(aspirin, antacid, potassium chloride));

    do i = 1 to dim(all_meds);
        do j = 1 to dim(check_meds);
            if upcase(all_meds[i]) = upcase(check_meds[j]) then occur = "Y";
        end;

        if occur = "Y" then leave;
    end;

    if occur ^= "Y" then delete;
run;

Hope this helps you next time you need to cross-check multiple items across multiple columns! Happy hacking!

Updates: My colleague, Mazi Ntintelo has rightly pointed out that the commas within the macro’s scan functions should be wrapped as %str(,) and also that the do loop checking for conmeds can be optimised with a leave statement. Thanks, Mazi!

Significant Figures in SAS

For three significant figures, the SAS Institute provides the following code snippet to accomplish the task.

However, it is often useful to round to more or less than 3 significant figures. I’ve developed a macro to do so for my own use and am sharing the code below.

%macro _nsigfig(varin=, varout=, n=);
if &varin. = 0 then &varout. = 0;
else do;
	if int(&varin.) ^= 0 then do;
	    &varout. = round(&varin., 10**(int(log10(abs(&varin.))) + (1 - &n.)));
	end;
	else do;
	    &varout. = round(&varin., 10**(-1*(abs(int(log10(abs(&varin.)))) + &n.)));
	end;
end;
%mend _nsigfig;

Searching for a string in an entire SAS library

For the most part, experienced SAS programmers know where to look for the source data they need. In the pharmaceutical industry, we are familiar with CDISC standards and data structures.

However, should the data standard be unfamiliar or the source datasets include new or unusual parameters, it may be prudent to have SAS look through the data on your behalf to save time.

We can do this by making use of PROC CONTENTS and SAS macro loops with the forward re-scan rule. If you’re not familiar with the forward re-scan rule, see my other blog post covering the topic.

First, let us define the two input parameters we’ll need: the library we’re delving into, and the character string we’re looking for. These are the only two parameters the end user needs to edit.

/*Search parameters - LIBRARY and text to search for*/
%let lib = %str(sashelp);
%let searchtext = %str(cholesterol);

Next, we’ll find all the datasets in the library specified above by running PROC CONTENTS and packing the results into iterable macro variables. In this case we can allocate a maximum of 999 datasets, but you can increase this value as needed.

/*Find all datasets within the library and allocate into numbered macro variables DS1, DS2, ...*/
proc contents data=&lib.._all_ out=members noprint;
run;

proc sql noprint;
    select distinct memname into :ds1-:ds999 from members;
quit;
%let ds_num = &sqlobs.;

Specify an empty results dataset. As matches are found during iteration, they will be appended to this dataset.

/*Clear the results dataset. As matches are found, they will be appended here.*/
data results;
    set _null_;
run;

Next we will loop over the datasets allocated as DS1, DS2, DS… above. The logical outline of the loop is as follows:

/*The macro logic for looping over each dataset in DS1, DS2, ...*/
%macro loop;
    %do i = 1 %to &ds_num.;
        /*For each dataset in the library, output its metadata contents*/

        /*Check the metadata for matches of the search text. This includes the dataset name, as well as the column names and labels.*/

        /*Check the actual data values for matches of the search text.*/

        /*For the current dataset, set the metadata and row value results.*/

        /*For the output dataset append the results of each loop iteration.*/
    %end;
%mend loop;
%loop;

For each step above, detailed logic follows below:

/*For each dataset in the library, output its metadata contents*/
proc contents data=&lib..&&ds&i out=c noprint;
run;
/*Check the metadata for matches of the search text. This includes the dataset name, as well as the column names and labels.*/
data metadata;
    length msg $500. dataset varname varlabel $100.;
    set c;
            
    /*Dataset name checking*/
    if index(upcase(memname), upcase("&searchtext.")) > 0 then do;
        msg = "Dataset: Found &searchtext.";
        dataset = "&&ds&i";
        varname = "";
        varlabel = "";
        output;
    end;

    /*Column name and label checking*/
    if index(upcase(name), upcase("&searchtext.")) > 0 or index(upcase(label), upcase("&searchtext.")) > 0 then do;
        msg = "Col Meta: Found &searchtext.";
        dataset = "&&ds&i";
        varname = name;
        varlabel = label;
        output;
    end;
            
    keep dataset varname varlabel msg;
            
    /*Get rid of duplicate messages in the case of multiple identical matches.*/
    proc sort nodupkey; by msg dataset varname varlabel;
run;
/*Check the actual data values for matches of the search text.*/
data actualdata;
    length msg $500. dataset varname varlabel varval $100.;
    set &lib..&&ds&i;
            
    /*Create an array of all character variables in the current dataset.*/
    array allChar _CHARACTER_;

    /*Loop over each character column and check for matches of the search text.*/
    do over allChar;
        if index(upcase(allchar), upcase("&searchtext.")) > 0 then do;
            msg = "Value: Found &searchtext.";
            dataset = "&&ds&i";
            varname = vname(allchar);
            varlabel = vlabel(allchar);
            varval = allchar;
            output;
        end;
    end;
    keep dataset varname varlabel varval msg;
            
    /*Get rid of duplicate messages in the case of multiple identical matches.*/
    proc sort nodupkey; by msg dataset varname varlabel varval;
run;
/*For the current dataset, set the metadata and row value results.*/
data allmsgs;
    set metadata actualdata;
    msg = upcase(msg);
run;
/*For the output dataset, OUT, keep appending the results of each loop iteration.*/
data results;
    set 
        results 
        allmsgs
    ;
run;

The steps above conclude the inner logic of the loop. Finally, outside the loop, we can clean up the datasets we no longer need.

/*Clean up*/
proc datasets library=work nolist;
    delete members c metadata actualdata allmsgs;
quit;

We started off with the SASHELP library, looking for the string “cholesterol”. These are the results:

Since we are searching in SASHELP, the results also include values from the SAS helper datasets, i.e., VCOLUMN and VMACRO.

This search function is not case sensitive. Optimise and implement as needed for your particular scenario.