Setting table value to macro variable SAS - sas

I need to store a value from a table into a variable. I've tried let, symputx, and select into. In the current version I try to use symputx, but the variable is not being updated. The products table contains type, price_floor, price_tier1, price_tier2.
%global price1;
%global price2;
%macro container();
DATA _null_;
SET products;
IF type = "Single" THEN DO;
CALL SYMPUTX('price1', price_floor,'g');
END;
IF type = "Multi" THEN DO;
CALL SYMPUTX('price1', price_tier1,'g');
CALL SYMPUTX('price2', price_tier2,'g');
END;
%PUT &=price1;
%PUT &=price2;
%mend;
Both price1 and price2 are null.
SYMBOLGEN: MACRO variable PRICE1 resolves to PRICE1=
SYMBOLGEN: MACRO variable PRICE2 resolves to PRICE2=

You don't have a run statement on your datastep, so your %put statement is being written to the log before the data step executes - so, the variables don't exist yet. It won't be run until you do provide a run statement or step boundary, or SAS might do that politely for you when the program is finished, but either way it's not being run before the %put.
Usually, this kind of program is an anti-pattern in SAS; you don't provide sufficient context, so maybe it's okay, but this will only work if you have only one row in the dataset - otherwise it probably won't do anything useful. The key word that's triggering me to write this is that you called this a "variable" - not a "macro variable" - in your question; SAS macro variables are not really "variables" and not meant to be used like a C variable or Python variable.

FYI to anyone interest, I decided to restructure the program as a whole. The original plan was to pass elements of a user defined array into %container, then use the assigned macro variables price1 and price2 as parameter in another macro call.
Instead of imbedding this data step in a macro, I created a table to contain all of the inputs I planned to pass into %container. Then I just used executes with the table variables concatenated within instead of direct macro calls.
data _null_;
SET products;
IF type = "Single" THEN DO;
CALL execute('%split_prod('||price_floor||');');
END;
IF type = "Multi" THEN DO;
CALL execute('%select_prod('||price1||','||price2||');');
END;
run;

There isn't anything wrong with the code besides the RUN.
%global price1;
%global price2;
data products;
infile cards dlm=',';
input Type $ price_floor price_tier1 price_tier2;
cards;
Multi, 40, 20, 60
;;;;;
%macro container();
DATA _null_;
SET products;
IF type = "Single" THEN DO;
CALL SYMPUTX('price1', price_floor,'g');
END;
IF type = "Multi" THEN DO;
CALL SYMPUTX('price1', price_tier1,'g');
CALL SYMPUTX('price2', price_tier2,'g');
END;
RUN;
%mend;
%container();
%PUT &=price1;
%PUT &=price2;
LOG:
96 %PUT &=price1;
PRICE1=20
97 %PUT &=price2;
PRICE2=60
You never showed your call for %container() so not sure what the underlying issue is, but CALL EXECUTE is a better method as debugging macros is painful.

Related

conditionally execute macro function

I want to conditionally execute macro function dependent on existence of table
Data
data have;
do i = 1 to 5;
output;
end;
run;
Table I want to condition on
data counttbl;
infile datalines delimiter='::';
format variable $char400. condition $char400.;
input variable $ condition $;
datalines;
runcount::i>1
;
run;
Some tests to show that I can condition on counttbl existence (works as expected)
data _null_;
call execute("data want;");
call execute("set have;");
if exist("work.counttbl") then
call execute("tmp = 1;");
call execute('run;');
stop;
run;
The above creates column tmp = 1
proc delete data=work.counttbl;
run;
data _null_;
call execute("data want;");
call execute("set have;");
if exist("work.counttbl") then
call execute("tmp = 1;");
call execute('run;');
stop;
run;
After deleting the table, the above does not create the column tmp
Macro function to execute
%macro apply_change();
call execute('if _n_ eq 1 then do;');
do until (eof);
set counttbl (keep=variable) end=eof;
call execute(strip(variable) || ' = 0;');
end;
call execute('end;');
call missing(eof);
do until (eof);
set counttbl end=eof;
call execute(strip(variable) || ' + (' || strip(condition) || ');');
end;
call missing(eof);
%mend apply_change;
Works fine when counttbl exists
data _null_;
call execute("data want;");
call execute("set have;");
if exist("work.counttbl") then
%apply_change()
call execute('run;');
stop;
run;
Throws error when counttbl is deleted - I want it to simply skip executing the macro function
proc delete data=work.counttbl;
run;
data _null_;
call execute("data want;");
call execute("set have;");
if exist("work.counttbl") then
%apply_change()
call execute('run;');
stop;
run;
ERROR: File WORK.COUNTTBL.DATA does not exist.
Thanks in advance for your help
Your problem area is
if exist("work.counttbl") then
%apply_change()
Macro is processed, and generates source code, prior to the SAS system implicitly compiling the data step and running it.
I would not recommend pursuing this avenue because it mixes scopes (macro/data step)
If you persist, there are a couple of tips
place all the code generation in a macro
place the existential check in the macro, and do NOT codegen source that has a SET counttbl when not present
For example:
%macro apply_change();
%if not %sysfunc(EXISTS(WORK.COUNTTBL)) %then %return;
%* This code gen only done when COUNTTBL present;
call execute('if _n_ eq 1 then do;');
do until (eof);
set counttbl (keep=variable) end=eof;
call execute(strip(variable) || ' = 0;');
end;
call execute('end;');
call missing(eof);
do until (eof);
set counttbl end=eof;
call execute(strip(variable) || ' + (' || strip(condition) || ');');
end;
call missing(eof);
%mend apply_change;
Replace
if exist("work.counttbl") then %apply_change()
With
%apply_change()
First
if exist("work.counttbl") then
will only apply to the first line of your macro
call execute('if _n_ eq 1 then do;');
This is because the macro is evaluated before the datastep is executed. So sas will simple paste the macro content to the location of the macro invocation.
But even when it would apply to the whole macro it will not work.
Take for example the following code:
data x;
if 0 then do;
set y;
end;
set z;
run;
Her y and z has to exist. However no observation will be read from y only structure is taken.
You cannot use a test at run time to prevent SAS from compiling some lines of code in the same data step. Instead you need to use macro logic to not generate the lines of code.
It looks like you want to use the dataset to generate a series of variables that count how many times a condition is met. I find that it is much easier to debug if that type of data driven code generation is done by writing the code to a file. Then you can stop after generating the file and look at the generated code and make sure your code generation step is working properly.
It looks like you want the new dataset generated whether or not the file with the list of VARIABLE/CONDITION pairs exists or not. So just hard code that part of the data step and only conditionally generate the part that calculates the new variables. Since you are generating sum statements there is not need for the IF _N_=1 block to set the initial values to zero. SAS will automatically set them to zero and retain them. (Assuming that HAVE doesn't already have variables with those names, in which the sum statement won't work right either.)
filename code temp;
data _null_;
file code ;
%if %sysfunc(exist(&dsname)) %then %do;
set &dsname end=eof;
put ' ' variable '+ ( ' condition ');' ;
%end ;
run;
So either the temp file CODE is empty or it has code like:
VAR1 + ( dx='123' );
VAR2 + ( sex='M' );
Then to make your dataset just run this step with a %INCLUDE to add in the conditionally generated code.
data want;
set have;
%include code /source2;
run;
If you are using an old version of SAS you will need to wrap that %IF statement into a macro. But the newest releases of SAS allow that type of simple %IF/%THEN/%DO/%END construct in open code.

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.

Do Loop using CALL SYMPUT

Having some problems with Do Loop Concepts. I have a static date (can be any date for that matter) defined with -
%LET DATE = %SYSFUNC(TODAY());
%PUT &DATE;
I need to create a series of macro variables that hold values of that date (&DATE) incremented by 10 days, so I used a simple data step to achieve this -
DATA _NULL_;
CALL SYMPUT('DATE10',&DATE+10);
CALL SYMPUT('DATE20',&DATE+20);
CALL SYMPUT('DATE30',&DATE+30);
RUN;
This method is OK for increments of 10 up to 30 days after the initial value of &DATE. I am now tasked with extending the report to execute on dates that extend to 250 days (incremented by 10 days) from the value of &DATE. Assuming a DO LOOP would be the most efficient execution method, I am having trouble understanding how the loop would "create" a new macro var (ex. &Date150) within the loop. Assuming the syntax below is correct, I am not sure what the next/correct step would be :
DATA _NULL_;
DO I=10 TO 150 BY 10;
CALL SYMPUT('DATE10',&DATE);
END;
RUN;
How would I "increment" the actual name of the macro var (&DATE10,&Date20...&Date150) in the loop while executing the creation of a macro var based on 10 day increments?
Use the I variable as part of your variable name, via a concatenation function, cats() is probably appropriate. Also, a personal preference, but I prefer Call SymputX as it removes any extra spaces.
DATA _NULL_;
DO I=10 TO 150 BY 10;
CALL SYMPUTX(cats('DATE', i), &DATE+i);
END;
RUN;
Consider placing the values into a single macro variable. As long as the list is not longer than maximum length of a macro variable.
DATA _NULL_;
length dates $32767 ;
date=today();
DO I=10 TO 150 BY 10;
dates=catx(' ',dates,date+i);
end;
CALL SYMPUTx('dates',dates);
RUN;
Then in your reporting code you can either just use the list of dates.
proc report ;
where date in (&dates);
...
run;
Or if you have macro you can use a %DO loop.
%do i=1 %to %sysfunc(countw(&dates));
%let date=%scan(&dates,&i);
proc report;
where date=&date;
....
%end;
Easy enough - pass in a variable as the first argument to call symput that contains the name of the macro variable you want to create.

Compress Newline character for dynamic varaibles

Dataset: Have
F1 F2
Student Section
Name No
Dataset "Have". Data has new line character.
I need to compress the newline character from the data.
I want to do this dynamically as sometimes the "Have" dataset may contain new variables like F3,F4,F5 etc.,
I have written as macro to do this.. However it is not working as expected.
When i execute the below code, first time I am getting error as invalid reference newcnt. If i execute for second time in the same session, i am not getting error.
PFB my code:
%macro update_2(newcnt);
data HAVE;
set HAVE;
%do i= 1 %to &newcnt;
%let colname = F&i;
&colname=compress(&colname,,'c');
%end;
run;
%mend update_2;
%macro update_1();
proc sql noprint;
select count(*) into :cnt from dictionary.columns where libname="WORK" and memname="HAVE";
quit;
%update_2(&cnt)
%mend update_1;
Note: All the variables have name as F1,F2,F3,F4.,
Please tell me what is going wrong..
If there is any other procedures, please help me.
In your macro %update_1 you're creating a macro variable called &cnt, but when you call %update_2 you refer to another macro variable, &colcnt. Try fixing this reference and see if your code behaves as expected.
We created our own function to clean unwanted characters from strings using proc fcmp. In this case, our function cleans tab characters, line feeds, and carriage returns.
proc fcmp outlib=common.funcs.funcs; /* REPLACE TARGET DESTINATION AS NECESSARY */
function clean(iField $) $200;
length cleaned $200;
bad_char_list = byte(10) || byte(9) || byte(13);
cleaned = translate(iField," ",bad_char_list);
return (cleaned );
endsub;
run;
Create some test data with a new line character in the middle of it, then export it and view the results. You can see the string has been split across lines:
data x;
length employer $200;
employer = cats("blah",byte(10),"diblah");
run;
proc export data=x outfile="%sysfunc(pathname(work))\x.csv" dbms=csv replace;
run;
Run our newly created clean() function against the string and export it again. You can see it is now on a single line as desired:
data y;
set x;
employer = clean(employer);
run;
proc export data=y outfile="%sysfunc(pathname(work))\y.csv" dbms=csv replace;
run;
Now to apply this method to all character variables in our desired dataset. No need for macros, just define an array referencing all the character variables, and iterate over them applying the clean() function as we go:
data cleaned;
set x;
array a[*] _char_;
do cnt=lbound(a) to hbound(a);
a[cnt] = clean(a[cnt]);
end;
run;
EDIT : Also note that fcmp may have some performance considerations to consider. If you are working with very large amounts of data, there may be other solutions that will perform better.
EDIT 6/15/2020 : Corrected missing length statement that could result in truncated responses.
Here's an example of Robert Penridge's function, as a call routine with an array as an argument. This probably only works in 9.4+ or possibly later updates of 9.3, when permanent arrays began being allowed to be used as arguments in this way.
I'm not sure if this could be done flexibly with an array as a function; without using macros (which require recompilation of the function constantly) I don't know how one could make the right size of array be returned without doing it as a call routine.
I added 'Z' to the drop list so it's obvious that it works.
options cmplib=work.funcs;
proc fcmp outlib=work.funcs.funcs;
sub clean(iField[*] $);
outargs iField;
bad_char_list = byte(11)|| byte(10) || byte(9) || byte(13)||"Z";
do _i = 1 to dim(iField);
iField[_i] = translate(iField[_i],trimn(" "),bad_char_list);
end;
endsub;
quit;
data y;
length employer1-employer5 $20;
array employer[4] $;
do _i = 1 to dim(employer);
employer[_i] = "Hello"||byte(32)||"Z"||"Goodbye";
end;
employer5 = "Hello"||byte(32)||"Z"||"Goodbye";
call clean(employer);
run;
proc print data=y;
run;
Here is another alternative. If newline is the only thing you want to remove, then we are talking about Char only, you may leverage implicit array and Do over,
data want;
set have;
array chr _character_;
do over chr;
chr=compress(chr,,'c');
end;
run;

Sas renaming variable with do loop and if then condition

I'm trying to rename variables x0 - x40 so that x0 will become y_q1_2014, x1 will become y_q4_2013, x2 will become y_q3_2013 and so on till x40 that will become y_q1_2004.
I want my new variable to display in its name the quarter and year of the observation. Now I have the following macro in SAS that is not working properly: the values of j and k are not changing according to the if - then condition. What am i doing wrong?
%macro rename(data);
%let j=1;
%let k=2014;
%do i = 0 %to 40 %by 1;
data mydata;
set &data.;
y_q&j._&k. = x&i.;
if &j.=1 then do k = &k.-1 and j = 4;
else do j=&j.-1;
run;
%end;
%mend;
This will likely be easier to do using the data step rather than a macro loop (as most things are!).
In this case, you have two problems:
How to mass-rename variables
How to convert x# to y_q#_####
An easy way to rename variables is to create a dataset with the variable names as rows, then create the new variable names. You can then pull that into a rename list very easily.
So something like this would do that.
*Create dataset with names in it.
data names;
set sashelp.vcolumn;
where memname='HAVE' and libname='WORK' and name =: 'X';
keep name;
run;
*some operation to determine new_name needs to go in that dataset also - coming later;
*Now create a list of rename macro calls.
proc sql;
select cats('%rename(var=',name,',newvar=',new_name,')')
into :renamelist separated by ' '
from names;
quit;
*Here is the simple rename macro.
%macro rename(var=,newvar=);
rename &var.=&newvar.;
%mend rename;
*Now do the renames. Can also go in a data step.
proc datasets lib=work;
modify have;
&renamelist.
quit;
How to convert is a more interesting question, and begs the question: is this a one time thing, or is this a repeated process? If it's a repeated process, does X0 always mean the most recent quarter in the data, or does it always mean q1 2014?
Assuming it is always the most recent quarter, you can use intnx to do this.
%let initdate='01JAN2014'd;
data have;
do x = 0 to 40;
qtr = intnx('QUARTER',&initdate,-1*x);
format qtr YYQ.;
output;
end;
run;
You can thus use this code (the portion inside the do loop, operating on an x that you pull out of the name in the dataset) in the earlier names data step to create new_name however you want. You might use the YYQ format in your new name if you have flexibility here (as it's standard, and the easiest solution). Otherwise, you would want to pull this apart either using put and then substring, or quarter() and year() functions off of the date variable here.