How to prevent CATx functions from evaluating expression - sas

When calling CATT() function with %sysfunc, is there a way to stop it from evaluating an expression?
For example given the code:
%let date=10-13-2015;
%put %sysfunc(catt(The date Is:,&date));
I would like it to return:
The date Is:10-13-2015
Because 10-13-2015 is just a text string. But instead CATT() sees hyphen as a subtraction sign and evaluates it as a numeric expression, returning:
The date Is:-2018
I have tried macro quoting, but doesn't change anything, I suppose because I need to somehow hide the values from CATT(). Seems if any argument to CATT looks like an expression, it will be treated as such.
Another example:
%let value=2 and 3;
%put %sysfunc(catt(The value Is:,&value));
The value Is:1

Provided you can do so, just remove the comma - there's no need to separate it into an individual parameter (unless you're using catx() rather than catt():
%let date=10-13-2015;
%put %sysfunc(catt(The date Is: &date));
Personally, I think the best way to work is to store the date as a SAS date value and then use the second (optional) parameter of %sysfunc to apply the formatting. This provides better flexibility.
%let date = %sysfunc(mdy(10,13,2015));
%put The date Is: %sysfunc(sum(&date),mmddyyd10.);
If you are insistent on the original approach and are using catx(), then I don't know how to do it exactly. The closest I could get was to insert a piece of text so it couldn't be interpreted as an expression, and then remove that text afterwards using tranwrd. Pretty, ugly, and it leaves a space:
%let date=10-13-2015;
%let tmp=%sysfunc(catx(#, The date Is: , UNIQUE_STRING_TO_REMOVE&date ));
%let want=%sysfunc(tranwrd(&tmp, UNIQUE_STRING_TO_REMOVE, ));
%put &want;
Gives:
The date Is:# 10-13-2015
I also tried every combination of macro quoting, and scanned through the entire SAS function list and couldn't see any other viable options.

I don't see an easy way around this, unfortunately. I do see that you could in theory pass this through an FCMP function, though since FCMP doesn't allow true variable arguments, that isn't ideal either, but...
proc fcmp outlib=work.funcs.funcs;
function catme(delim $, in_string $) $;
length _result $1024;
length _new_delim $1;
_new_delim = scan(in_string,1,delim);
do _i = 1 to countc(in_string,delim);
_result = catx(_new_delim, _result, scan(in_string,_i+1,delim));
end;
return(_result);
endfunc;
quit;
options cmplib=work.funcs;
%let date=10-13-2015;
%put %sysfunc(catme(|,:|The date Is| &date.));
Or add quotes to the argument and then remove them after the CATx.
%sysfunc(dequote(%sysfunc(catt(.... ,"&date."))))
All messy.

The problem with %SYSFUNC() evaluating the arguments is not limited to the CAT() series of functions. Any function that accepts numeric values will result in SAS attempting to evaluate the expression provided.
This can be a useful feature. For example:
%let start_dt=10OCT2012 ;
%put %sysfunc(putn("&start_dt"d +1,date9));
You don't need to use CAT() functions to work with macro variables. Just expand the values next to each other and the are "concatenated".
%let date=10-13-2015;
%put The date Is:&date;
If you want to make a macro that works like the CATX() function then that is also not hard to do.
%macro catx /parmbuff ;
%local dlm return i ;
%if %length(&syspbuff) > 2 %then %do;
%let syspbuff = %qsubstr(&syspbuff,2,%length(&syspbuff)-2);
%let dlm=%qscan(&syspbuff,1,%str(,),q);
%let return=%qscan(&syspbuff,2,%str(,),q);
%do i=3 %to %sysfunc(countw(&syspbuff,%str(,),q));
%let return=&return.&dlm.%qscan(&syspbuff,&i,%str(,),q);
%end;
%end;
&return.
%mend catx;
%put %catx(|,a,b,c);
a|b|c
%put "%catx(",",a,b,c,d)";
"a","b","c","d"

Slightly less insane function-style macro without the dosubl:
%macro catx() /parmbuff;
%local rc dlm i params OUTSTR QWORD outstr;
%let SYSPBUFF = %qsubstr(&SYSPBUFF,2,%length(&SYSPBUFF)-2);
%let dlm = %qscan(&SYSPBUFF,1,%str(,));
%let params = %qsubstr(&SYSPBUFF,%index(&SYSPBUFF,%str(,))+1);
%let i = 1;
%let QWORD = %scan(&PARAMS,&i,%str(,));
%let OUTSTR = &QWORD;
%do %while(&QWORD ne);
%let i = %eval(&i + 1);
%let QWORD = %scan(&PARAMS,&i,%str(,));
%if &QWORD ne %then %let OUTSTR = &OUTSTR.&DLM.&QWORD;
%end;
%unquote(&OUTSTR)
%mend catx;
%put %catx(%str( ),abc,10 - 1 + 2,def);
Somewhat more insane but apparently working option - use %sysfunc(dosubl(...)) and lots of macro logic to create a function-style macro that takes input in the same way as %sysfunc(catx(...)), but forces catx to treat all input as text by quoting it and calling it in a data step.
%macro catxt() /parmbuff;
%local rc dlm i params QPARAMS QWORD outstr;
%let SYSPBUFF = %qsubstr(&SYSPBUFF,2,%length(&SYSPBUFF)-2);
%let dlm = %qscan(&SYSPBUFF,1,%str(,));
%let params = %qsubstr(&SYSPBUFF,%index(&SYSPBUFF,%str(,))+1);
%let i = 1;
%let QWORD = "%scan(&PARAMS,&i,%str(,))";
%let QPARAMS = &QWORD;
%do %while(&QWORD ne "");
%let i = %eval(&i + 1);
%let QWORD = "%scan(&PARAMS,&i,%str(,))";
%if &QWORD ne "" %then %let QPARAMS = &QPARAMS,&QWORD;
%end;
%let rc = %sysfunc(dosubl(%str(
data _null_;
call symput("OUTSTR",catx("&dlm",%unquote(&QPARAMS)));
run;
)));
&OUTSTR
%mend catxt;
%put %catxt(%str( ),abc,10 - 1 + 2,def);
Although this uses a data step to execute catx, dosubl allows the whole thing to be run in any place where you could normally use %sysfunc(catx(...)).

Related

Check whether item is in SAS list

I have a SAS list. This SAS list is stored in a macro variable. Please assume that I have no table to derive this SAS list.
The SAS list contains names separated by commas. An example of the SAS list macro variable:
%LET sas_list = name1,name2,name3;
I want to check whether macro-variable “item” is present in the list.
Something like:
%IF &item. IN &sas_list. %THEN %DO;
Whatever;
%END;
For some reason, I get the error:
“A character operand was found in the %EVAL function or %IF condition where a numeric operand is required. The condition was: name1 IN name1,name2,name3”
Help is appreciated.
You need to use two options here:
MINOPERATOR, this will allow the use of the IN operator
MINDELIMITER, this will allow you to set the delimiter
%let sas_list = name1,name2,name3;
options minoperator mindelimiter=',';
%Macro want(item);
%if &item. in &sas_list. %then %put i = 1;
%else %put i = 0;
%mend;
%want(name1);
i = 1
Add the minoperator and mindelimiter system options. These stand for Macro IN Operator and Macro IN Delimiter.
options minoperator mindelimiter=',';
%LET sas_list = name1,name2,name3;
%LET item = name1;
%IF &item. IN &sas_list. %THEN %DO;
%put &item is in &sas_list;
%END;
Output:
name1 is in name1,name2,name3
If you want to find values that are not in a list, pass it through %eval.
options minoperator mindelimiter=',';
%LET sas_list = name1 name2 name3;
%LET item = name4;
%IF %eval(&item. IN &sas_list.) = 0 %THEN %DO;
%put &item NOT in &sas_list;
%END;
Output:
name4 is NOT in name1,name2,name3
Note that you can also supply these options directly in a macro if you only want in to work selectively. For example:
%macro foo / minoperator mindelimiter=',';
...
%mend;
You can also use the FINDW funtion to ascertain the presence of an item in a list.
%if %sysfunc(FINDW(%upcase(%superq(saslist)), %upcase(&item), %str(,))) %then %do;
...
%end;
Supporting
%LET sas_list = name1,name2,name3;
you can find if name2 is in the list this way.
data test;
it = findw("&sas_list", 'name2',',');
run;
but I assume you want to find it out without using a data step, so
%let het = %sysfunc(findw(%quote(&sas_list),name2,%quote(,)));
%put NOTE: het is &het;
does the job. Note
you find is not a macro function, so you need %sysfunc() to call it in a macro statement
you have to mask the comma's with %quote(). Otherwise they would be considered argument separators.

How do I create a SAS variable from the output of a MACRO

I have a dataset with each observation having two space-separated lists as string variables. I want a third variable showing the overlap between the string lists. Using another SO post, I've created a macro to calculate the overlap. I can't work out how to implement it in a DATA step to get the third variable.
This is my dataset, with dummy data:
data use;
infile datalines dlm='~~';
input list1:$100. list2:$100. expected_match:$10.;
datalines;
Homer Bart~~Homer Bart~~Full
Marge Lisa~~Lisa Marge~~Full
Homer Marge~~Marge~~Partial
Bart Lisa~~Bart~~Partial
Homer Marge Bart Lisa~~Maggie~~None
;;;;
run;
This is the macro, with tests (all of which pass):
%macro list_overlap(list1, list2);
%local i matches match_type;
%let matches = 0;
%do i = 1 %to %sysfunc(countw(&list1, %str( )));
%if %sysfunc(findw(&list2, %scan(&list1, &i,, s)))
%then %let matches = %eval(&matches + 1);
%end;
%if &matches = %sysfunc(countw(&list1, %str( )))
and %sysfunc(countw(&list1, %str( ))) = %sysfunc(countw(&list2, %str( )))
%then %let match_type = 'Full';
%else %if &matches = 0 %then %let match_type = 'None';
%else %let match_type = 'Partial';
match_type = &match_type%str(;)
%mend list_overlap;
%put NOTE: %list_overlap(Homer Bart,Homer Bart);
%put NOTE: %list_overlap(Marge Lisa,Lisa Marge);
%put NOTE: %list_overlap(Homer Marge,Marge);
%put NOTE: %list_overlap(Bart Lisa,Bart);
%put NOTE: %list_overlap(Homer Marge Bart List,Maggie);
This is how I'm trying to implement it in a DATA step:
data matches;
set use;
call execute(catt('%list_overlap(', list1, ',', list2, ')'));
run;
I'm getting the following error with this case:
NOTE: Line generated by the CALL EXECUTE routine.
1 + match_type = 'Full';
__________
180
ERROR 180-322: Statement is not valid or it is used out of proper order.
I've tried other ways too, but this is the closest I've got.
Looks like you want the RESOLVE() function instead of CALL EXECUTE.
data matches;
set use;
match_type = resolve(cats('%list_overlap(',list1,',',list2,')'));
run;
But with your current definition of the macro that will include all of characters the macro generates, match_type = 'Full';, into the value of the MATCH_TYPE variable. So remove the superfluous characters the macro is currently generating so that it only generates the value you want to save.
... %then Full;
%else %if matches eq 0 %then None;
%else Partial;
Your problem here is that call execute isn't doing what you think, I suspect.
What's happening:
Data step runs, call execute lines generated
Then the macro stuff is call executed, and you have:
Code example:
data matches;
set use;
call execute(stuff);
run;
match_type = 'Full';
That's not legal - that's a data step line but not in a data step.
Instead of doing all of that work in macro land, do it in data step land. Works just as well, and gets done what you want.
Something like this:
%macro list_overlap(list1,list2);
matches=0;
length match_type $7;
do i = 1 to countw(&list1,' ');
if findw(&list2,scan(&list1,i,' '))
then matches = matches + 1;
end;
if matches eq countw(&list1,' ') then match_type = 'Full';
else if matches eq 0 then match_type = 'None';
else match_type = 'Partial';
%mend list_overlap;
Something like that, I can't test it right now, but that should generally work. Then don't call execute the macro, just call it normally.
data matches;
set use;
%list_overlap(list1,list2);
run;

passing date variable to macro for sysfunc processing

below is my little problem to create a macro and passing in a date variable. Without using the date variable, it works with results as below.
%macro x();
%let i=-1;
%let dts = %sysfunc(today());
%put &dts; /*ok*/
%let yymm1 = %sysfunc(intnx(MONTH,&dts,&i));
%put &yymm1; /*ok*/
%let mth_beg = %sysfunc(intnx(MONTH,&dts,&i,B),date9.);
%let mth_end = %sysfunc(intnx(MONTH,&dts,&i,E),date9.);
%put &mth_beg &mth_end; /*01JAN2018 31JAN2018*/
/*** proc sql code below ** */
%mend;
%x();
log:
21231
21185
01JAN2018
31JAN2018
Now I create a macro around it and got the following error:
%macro x(dts1);
%let i=-1;
/*%let dts = %sysfunc(today());*/
%let dts = %sysfunc(&dts1);
%put &dts; /*ok*/
%let yymm1 = %sysfunc(intnx(MONTH,&dts,&i));
%put &yymm1; /*ok*/
%let mth_beg = %sysfunc(intnx(MONTH,&dts,&i,B),date9.);
%let mth_end = %sysfunc(intnx(MONTH,&dts,&i,E),date9.);
%put &mth_beg &mth_end; /*01JAN2018 31JAN2018*/
/*** proc sql code below ** */
%mend;
%x(16JAN2018);
ERROR: Function name missing in %SYSFUNC or %QSYSFUNC macro function reference.
JAN2018)
ERROR: Expected close parenthesis after macro function invocation not found.
))
ERROR: Expected close parenthesis after macro function invocation not found.
ERROR: Expected close parenthesis after macro function invocation not found.
,B),date9.) ,E),date9.)
I am not sure how to let SAS treat the date passed in as a recognized date. I know i probably used the sysfunc(&dts) wrongly or the date passed in need to adhere to certain format. i just want the date to replace today(). Can you help? I am a SAS newbie.
thanks
Wrap the date in " and end with a d. That will tell SAS to convert the string to a date:
%macro x(dts1);
%let i=-1;
/*%let dts = %sysfunc(today());*/
%let dts = "&dts1"d; /*Change here!*/
%put &dts; /*ok*/
%let yymm1 = %sysfunc(intnx(MONTH,&dts,&i));
%put &yymm1; /*ok*/
%let mth_beg = %sysfunc(intnx(MONTH,&dts,&i,B),date9.);
%let mth_end = %sysfunc(intnx(MONTH,&dts,&i,E),date9.);
%put &mth_beg &mth_end; /*01JAN2018 31JAN2018*/
/*** proc sql code below ** */
%mend;
%x(16JAN2018);
change %let dts = %sysfunc(&dts1); to
%let dts = %sysfunc(inputn(&dts1,date9. ));
SAS stores dates as the number of days since 01JAN1960. So if you do not attach a date format to the date value it will just look like an integer.
%let today=%sysfunc(today());
You can then use that integer anywhere you would use a date value.
%let next_month=%sysfunc(intnx(month,&today,1,b));
You can also represent dates by using a date literal. To make a date literal you represent the date value using something the DATE informat can read (like 16FEB2018, 16feb18, 16-FEB-2018, etc.) enclosed in quotes with the letter d appended.
%let today="%sysfunc(today(),date9)"d ;
%let date_string=13FEB2018;
%let date_value="&date_string"d ;
So date literals will work in SAS code and when you use the %sysfunc() macro function to call a SAS function (like INTNX) and they will work in the %sysevalf() macro function. But the %eval() macro function will not recognize date literals. So you will need to use %sysevalf() if you want use arithmetic or comparisons of date literals in macro logic.
%if %sysevalf(&today > '01JAN2018'd) %then ....
%let tomorrow=%sysevalf(&today +1);

How do I work out the data type of my macro variable in SAS

How do I print out the data type of a macro variable in the log
%macro mymacro(dt2);
%LET c_mth = %SYSFUNC(intnx(month,&dt2.d,-1,e),date9.) ;
%put &c_mth;
%mend;
mymacro('01sep2014')
I have a bunch of macro variables assigned using a %let or into:
my problem is I'm trying to do a bunch of boolean condition on dates but I suspect that some of my variables are strings and some are dates
I have casted them in my code but to triple check there is surely a way to return something to the log
I want something similar to using str() or mode() or is.numeric() in R
H,
The SAS macro language is weird. : )
As Reeza said, macro variables do not have a type, they are all text.
But, if you use Boolean logic (%IF statement), and both operands are integers, the macro language will do a numeric comparison rather than a character comparison.
So you can use the INPUTN() function to convert the date strings to SAS dates (number of days since 01Jan1960), and then compare those. Here's an example, jumping off from your code:
%macro mymacro(dt1,dt2);
%local c_mth1 c_mth2 n_mth1 n_mth2;
%let c_mth1 = %sysfunc(intnx(month,&dt1.d,-1,e),date9.) ;
%let c_mth2 = %sysfunc(intnx(month,&dt2.d,-1,e),date9.) ;
%let n_mth1 = %sysfunc(inputn(&c_mth1,date9.)) ;
%let n_mth2 = %sysfunc(inputn(&c_mth2,date9.)) ;
%put &c_mth1 -- &n_mth1;
%put &c_mth2 -- &n_mth2;
%if &n_mth1<&n_mth2 %then %put &c_mth1 is before &c_mth2;
%else %put &c_mth1 is NOT before &c_mth2;
%mend;
Log from a sample call:
236 %mymacro('01feb1960','01mar1960')
31JAN1960 -- 30
29FEB1960 -- 59
31JAN1960 is before 29FEB1960
--Q.
Macro variables do not have a type, they are all text.
You have to make sure the variable is passed in a way that makes sense to the program and generates valid SAS code.
%let date1=01Jan2014;
%let date2=31Jan2014;
data _null_;
x = "&date1"d > "&date2"d;
y = "&date2"d > "&date1"d;
z = "&date2"d-"&date1"d;
put 'x=' x;
put 'y=' y;
put 'z=' z;
run;
Log should show:
x=0
y=1
z=30
If your macro variables resolve to date literals, you can use intck combined with %eval to compare them, e.g.
%let mvar1 = '01jan2015'd;
%let mvar2 = '01feb2015'd;
/*Prints 1 if mvar2 > mvar1*/
%put %eval(%sysfunc(intck(day,&mvar1,&mvar2)) > 0);

How to use SAS Macro call in a %let

I want use a macro in a %let call, Below is the Macro code and how I want to invoke it. Please help me achieve it.
%macro xscan(string, delimiter, word_number);
%let len1=%length(&string); /*Computing the length of the string*/
%let len=%eval(&len1+1);
%let sub=%scan(&string,&word_number,"&delimiter"); /*Fetch the string specified by word_number*/
%if &word_number ge 0 %then %do;
%let pos=%index(&string,&sub); /* Locate the position while reading left to right*/
%end;
%if &word_number lt 0 %then %do;
data _null_;
pos=find("&string","&sub",-&len); /* Locate the position while reading from right to left*/
call symput("pos",pos);
run;
%end;
%let strg=%substr(&string,&pos); /* Extract the substring*/
%put the string is &strg;
%mend;
%let sub_str = %xscan(a bb ccc dddd bb eeeee, %str( ), -2);
%put The value of sub_str = &sub_str;
Desired implementation:
data work.in_data;
length in_string $50;
in_string = “a bb ccc dddd bb eeeee”;
output;
in_string = “aa b cc aa dee”;
output;
run;
data work.out_data;
set work.in_data;
length sub_str $50;
start_word_num = -(_n_ +1);
sub_str = %xscan(in_string,’ ‘, start_word_num);
run;
proc print; run;
I'm posting a new answer since the other answer answers a slightly different question.
Here, your macro really is intended to perform data step techniques, not macro techniques. You cannot (easily) use a macro to edit variable contents; a macro is intended to write SAS code, not to modify variables. You could use PROC FCMP to solve this problem, and I may well do so if I have more time, but for now here's the proper solution with just data step techniques and a normal (non-functional) macro.
First, write the data step technique to accomplish it. This is a fairly messy but effective solution. It only works for negative start_word_num; if left or right is desired it would need some modification to the loop parameters. I suggest using this as a starting point and improving it for your needs.
data work.out_data;
set work.in_data;
length sub_str $50;
start_word_num = -(_n_ +1);
do _t = countc(trimn(in_string),' ')+1 to countc(trimn(in_string),' ')+start_word_num+2 by -1;
sub_str = catx(' ',scan(in_string,_t,' '),sub_str);
put _t= sub_str=;
end;
put in_string= sub_str=;
run;
Now, move the loop into a macro.
%macro xscan(word_num, initial_string, result);
&result.=' ';
do _t = countc(trimn(&initial_string.),' ')+1 to countc(trimn(&initial_string.),' ')+&word_num.+2 by -1;
&result. = catx(' ',scan(&initial_string.,_t,' '),&result.);
end;
%mend xscan;
data work.out_data;
set work.in_data;
length sub_str $50;
start_word_num = -(_n_ +1);
%xscan(start_word_num,in_string,sub_str);
put in_string= sub_str=;
run;
You have two problems. First off, a function-style macro must not contain any data steps (or procs or anything else). If you do need to execute a data step, you have to use FCMP with run_macro. However, here you can use %SYSFUNC to accomplish what you are doing in the data step.
Second, you need to actually return the value. Ultimately a macro resolves to text, so you need to resolve
%let x = %xscan(...);
to
%let x = bb eeeee;
So you need to simply have bb eeeee as open text in your macro.
This should accomplish both things:
options mprint symbolgen;
%macro xscan(string, delimiter, word_number);
%local len1 len sub pos;
%let len1=%length(&string); /*Computing the length of the string*/
%let len=%eval(&len1+1);
%let sub=%scan(&string,&word_number,"&delimiter"); /*Fetch the string specified by word_number*/
%if &word_number ge 0 %then %do;
%let pos=%index(&string,&sub); /* Locate the position while reading left to right*/
%end;
%else %if &word_number lt 0 %then %do;
%let pos=%sysfunc(find(&string,&sub,-&len)); /* Locate the position while reading from right to left*/
%end;
%substr(&string,&pos) /* Extract the substring*/
%mend;
%let sub_str = %xscan(a bb ccc dddd bb eeeee, %str( ), -2);
%put The value of sub_str = &sub_str;
(Note, I don't necessarily know this does what you really want, but it does what the code appears to be doing.)
Some tips for function-style macros, courtesy of Rob Penridge:
Define all of your macro variables using a %local statement like so: %local len1 len sub pos;. That way you do not overwrite global macro variables.
Use /* THIS STYLE FOR COMMENTING */. Using other comment styles may cause the line to end.
The secret to making the macro work is the line that uses %substr at the end. This resolves to bb eeeeee being left in open code. Since that is all that is left, that is what calling the macro resolves to.
Do not put a semicolon on the line that is actually returned, as it may be undesirable when the function-style macro is used.