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.
Related
I'm trying to write a macro to replace some current code. The current code is like this:
data _null_;
call symput('as_of_date_nbr1', put(intnx('month', today(),-1, 'E'), yymmddn.));
run;
%put &as_of_date_nbr1 yields 20210831
There is a data_null for each of 30 months. That variable is then used in a data step where statement
where date = &as_of_date_nbr1
I want to write a macro where the variable as_of_date_nbr would take a macro variable from 1-30 and the intnx function would also take a macro variable from 1-30
So something like this if possible in a loop
data _null_;
call symput('as_of_date_nbr.&macvar, put(intnx('month,today(),-&macvar, 'E'), yyyymmddn.))
So far I'm not having any luck, any ideas would be appreciated.
First start by trying to create a data set with the 30 dates needed.
data demo;
do i=1 to 30;
var_name=catt('month', put(i, z2. -l));
var_value=intnx('month', today(), -1*i, 'e');
format var_value yymmddn8.;
output;
end;
run;
Once you're sure that's correct, you can add a CALL SYMPTUX line to create the macro variables.
call symputx(var_name, put(var_value, yymmddn8.), 'g');
Depending on what you're doing overall, you may want to explore CALL EXECUTE. For examle if you were looping a macro with these values instead of call symputx you could use CALL EXECUTE() to just call the macro directly with the values instead.
So if you had a macro called %filter like this:
%macro filter(date_param);
sas code
%mend;
You could call it instead as follows:
call execute(catt('%filter_data(', put(var_value, yymmddn8.), ');'));
All together it would be something like this:
data demo;
do i=1 to 30;
var_name=catt('month', put(i, z2. -l));
var_value=intnx('month', today(), -1*i, 'e');
format var_value yymmddn8.;
call symputx(var_name, put(var_value, yymmddn8.), 'g');
*call execute(catt('%filter_data(', put(var_value, yymmddn8.), ');'));
output;
end;
run;
*check macro variables created properly;
%put &month01.;
%put &month15.;
%put &month30.;
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.
I have this code :
%global nb_usag;
%global usager_entr;
%let nb_usag=0;
%syslput nb_usag=&nb_usag;
%let usager_entr=u;
PROC SQL noprint ;
select count(distinct no_usager_entr) into :nb_usag from &lib..INSCRITS_USA_1
;
quit;
data _null_;
if &nb_usag > 0 then do;
call execute
("PROC SQL noprint ;
select distinct no_usager_entr INTO :usager_entr separated by ','
from &lib..INSCRITS_USA_1;")
;
if &usager_entr ne "u" then do;
call prxchange('s/,/\",\"',-1,&usager_entr);
end;
end;
run;
%let usager_entr="&usager_entr";
%syslput usager_entr=&usager_entr;
%put &nb_usag;
%put &usager_entr;
But the code generate this error for the function prxchange:
ERROR 135-185: Attempt to change the value of the constant 's/,/\",\"' in the PRXCHANGE subroutine call.
What I am doing wrong?
I want to modify every , in the variable usager_entr with ",".
For example, if usager_entr = 12121212,34343434,56565656 it will become 12121212","34343434","56565656.
In my case the table &lib..INSCRITS_USA_1 is empty, then nb_usag=0.
Thanks!
Too much fooling around! Instead, create the double quoted list of values directly from Proc SQL
Presuming the list will be used in a later clause with the construct IN (&myList).
%let usager_entr_dq_csv_list = "redundant safety value that will never match anything";
PROC SQL noprint ;
select distinct quote(trim(no_usager_entr))
INTO :usager_entr_dq_csv_list separated by ','
from &lib..INSCRITS_USA_1;
The redundant value is needed because if it were empty you would later generate IN () and have a syntax error.
If the list is being used in pass-through SQL you will want to use the additional arguments of QUOTE function to bound the values with a single quote.
For modify your macro variable from usager_entr = 12121212,34343434,56565656 to "12121212","34343434","56565656", you could use prxchange:
%let usager_entr =12121212,34343434,56565656;
%put &usager_entr;
%let New_usager_entr=%sysfunc(prxchange(s/([1-9]+)/"$1"/, -1,%quote(&usager_entr)));
%put &New_usager_entr;
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;
Here is the macro code.....
libname myfmt "&FBRMrootPath./Formats";
%macro CreateFormat(DSN,Label,Start,fmtname,type);
options mprint mlogic symbolgen;
%If &type='n' %then %do;
proc sort data=&DSN out=Out; by &Label;
Run;
Data ctrl;
set Out(rename=(&Label=label &Start=start )) end=last;
retain fmtname &fmtname type &type;
output;
If last then do;
hlo='O';
label='*ERROR';
output;
End;
Run;
%End;
%Else %do;
proc sort data=&DSN out=Out; by &Start;
Run;
Data ctrl;
set Out(rename=(&Start=label &Label=start )) end=last;
retain fmtname &fmtname type &type;
output;
If last then do;
hlo='O';
label='*ERROR';
output;
End;
Run;
%End;
proc format library=myfmt cntlin=ctrl;
Run;
%Mend CreateFormat;
Here is the code for control data set through which above macro should run for each observation of the data set and the values of the observations are inputs for varibales in the macro....
Data OPER.format_control;
Input DSN :$12. Label :$15. Start :$15. fmtName :$8. type :$1. fmt_Startdt :mmddyy. fmt_Enddt :mmddyy.;
format fmt_Startdt fmt_Enddt date9.;
Datalines;
ssin.prd prd_nm prd_id mealnm n . 12/31/9999
ssin.prd prd_id prd_nm mealid c . 12/31/9999
ssin.fac fac_nm onesrc_fac_id fac1SRnm n . 12/31/9999
ssin.fac fac_nm D3_fac_id facD3nm n . 12/31/9999
ssin.fac onesrc_fac_id D3_fac_id facD31SR n . 12/31/9999
oper.wrkgrp wrkgrp_nm wrkgrp_id grpnm n . 12/31/9999
;
Something like this.
proc sql;
select catx(',',cats('%CreateFormat(',DSN),Label,Start,fmtname,cats(type,')');
into :formcreatelist separated by ' '
from oper.format_control;
quit;
You may need to PUT some of your variables to get the format you want into the macro variable. I use the slightly cludgy cats/catx combo here, you could cats once with ',' added in a bunch of times also.
You do have a limit here - around 20,000 characters total in a macro variable. If it's over that, you either have to use CALL EXECUTE (which has some quirky features) or you can put the macro call into a text file and %INCLUDE it.
There is a better way to do this rather than select ... into a macro variable. Use a temp file like this:
filename dyncode temp;
data _null_;
file dyncode;
set OPER.format_control;
put '%createformat ....';
run;
%include dyncode;
filename dyncode clear;
This technique is not limited by the 32k length limitation on macro variables.
Note that you should definitely use single quotes around the %createformat to prevent SAS from invoking the macro just prior to data step compilation. You want the macro to run when the %include runs.
The above approach is analogous to call execute, but call execute is evil because it does not execute the macro and embedded data/proc code within the macro in the expected order. Avoid call execute.
Finally, if you are running interactive SAS and using the technique there is a neat trick you can use to debug. Comment out the last two lines of code -- the include and the filename clear. After you run the remaining code, enter the SAS command "fslist dyncode" in the command window. This will pop up a notepad view on the dynamic code you just generated. You can review it and make sure you got what you intended.
Here's a call execute solution, just for completeness:
data _null_;
set OPER.format_control;
call execute('%CreateFormat(' || DSN || ',' || Label || ',' || Start || ',' || fmtname || ',' || type || ');');
run;