I have this macro that doesn't resolve even if the logic seems to work. I'm a beginner sas user so the code might look a little messy.
%let macro1 = 1600;
%let macro2 = 1300;
%if '¯o1.' > '¯o2.' %then %do;
%let macro3 = increase;
%else %if '¯o1.' < '¯o2.' %then %do;
%let macro3 = decrease;
%else %if '¯o1.' = '¯o2.' %then %do;
%let macro3 = stability;
%end;
when I run the code, there are no errors but macro3 does not resolve and it looks something like:
"There is a ¯o3. between 1600 and 1300"
You are missing %END; statements for the %DO; statements. Do you really need the %DO; statements? You don't appear to be trying to run multiple statements when the condition is true.
The macro processor will ignore macro triggers that are inside quoted strings that are bounded by single quote characters. So the result of your tests will always be that the string '¯o1' is less than the string '¯o2' because the digit 1 comes before the digit 2 in lexicographical ordering.
Either remove the quotes completely or replace them with double quote characters.
Without the quotes the implied %EVAL() macro function call will compare the strings 1600 and 1300 as integer numbers. With the quotes then %EVAL() will compare the string "1600" and "1300" as character strings.
So if the values of MACRO1 and MACRO2 are supposed to be numbers then do not include the quotes in the %IF conditions. Otherwise values like "1200" will be less that values like "800" becuase 8 is larger than 1.
You also have to wrap the whole sequence of %IF/%THEN/%ELSE/%IF inside a macro definition, if it is not already inside of a macro definition, because you cannot have nested %IF in open code.
%macro testit;
%let macro1 = 1600;
%let macro2 = 1300;
%if ¯o1. > ¯o2. %then %let macro3 = increase;
%else %if ¯o1. < ¯o2. %then %let macro3 = decrease;
%else %let macro3 = stability;
%put &=macro3 ;
%mend testit;
%testit;
And if &MACRO1 and &MACRO2 are not INTEGER values then you will need to explicitly use %SYSEVALF() to compare them.
%let macro1 = 16.50;
%let macro2 = 13.00;
%if %sysevalf(¯o1. > ¯o2.) %then ....
Related
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.
I am trying to do a string comparison, between a string that will be read from a config file and a string that I mention. Is the below correct?
%if &strategy ne 'ABC' %then %do;
if ctry eq 'CAN' or ctry eq 'US' then maxpos = 0;
%end;
%else %do;
if ctry eq 'US' then maxpos = 0;
strategy is the parameter that will be read from a config file in which I will specify strategy = ABC
does ABC have to be specified in quotes?
Is the use of ne (not equal) correct?
Macro language doesn't naturally use quotes for the most part (in comparisons like this, they're treated more or less as normal characters, not as string-enclosures), so it depends on whether &strategy contains the quote character or not.
%let strategy=ABC;
%if &strategy = 'ABC' %then %put equals; %else %put not equals;
...
not equals
but
%let strategy='ABC';
%if &strategy = 'ABC' %then %put equals; %else %put not equals;
...
equals
You would generally compare %if &strategy eq ABC in most cases.
ne and eq are fine, or you can use = and ^=, up to you - I prefer ne.
I'm trying to do a date comparison but I'm not getting the correct results. Does anyone know what's going on?
%macro ttt;
%let check_start = 28APR2014;
%if "&check_start."d < "25may2014"d %then %let true = 1;
%else %if "&check_start."d > "25may2014"d %then %let true = 2;
%put &true;
%mend;
%ttt;
14 %macro ttt;
15 %let check_start = 28APR2010;
16 %if "&check_start."d < "25may2014"d %then %let true = 1;
17 %else %if "&check_start."d > "25may2014"d %then %let true = 2;
18 %put &true;
19 %mend;
20 %ttt;
true = 2
Macro-variable true should equal 1
You need to use %sysevalf() to evaluate the comparison in this case. The following works.
%macro ttt;
%let check_start = 28APR2015;
%if %sysevalf("&check_start"d < '25may2014'd) %then %let true=1;
%else %if %sysevalf("&check_start."d > '25may2014'd) %then %let true=2;
%put &true.;
%mend;
%ttt;
Reeza has provided a good solution but I thought I'd add a few suggestions as well.
The problem you are having is the reason I recommend never using date literals when working in the macro language. Instead, of date literals (i.e. "01jan2000"d) I recommend using macro variables that contain date values (i.e. %let start_of_21st_century = %sysfunc(mdy(1,1,2000)); ). By using macro variables, not only do you avoid your above issue, but you also get the benefit of being able to self-document your code.
Currently I have no idea what significance the 25th May 2014 has in your code, but if you had this line:
%let product_launch_date = %sysfunc(mdy(5,25,2014));
... then it would be clear to anyone reading it what the significance is.
Your code would then become:
%macro ttt;
%local check_start compare_date;
%let check_start = %sysfunc(mdy(4,28,2014));
%let compare_date = %sysfunc(mdy(5,25,2014));
%if &check_start < &compare_date %then %let true = 1;
%else %if &check_start > &compare_date %then %let true = 2;
%put &true;
%mend;
There's still a few more things I'd consider changing. One thing I noticed is that if the 2 date values are equal, than true will not be assigned a value. So that should probably be remedied.
Also, in SAS, the typical concept of true/false is typically represented as follows:
A value of zero represents FALSE
Any non-zero number (including negatives) represents TRUE
So having a macro variable named true with either a value of 1 or 2 (both values would normally represent a value of true) may be confusing to some. I'd consider either renaming the macro variable, or using values of 0, and 1 (or other non-zero number).
Incorporating all of this, the macro would become something like:
%macro check_dates;
%local check_start compare_date;
%let check_start = %sysfunc(mdy(4,28,2014));
%let compare_date = %sysfunc(mdy(5,25,2014));
%let check_start_compared_higher = &check_start > &compare_date;
%if &check_start_compared_higher %then %do;
%put It was higher =) ;
%end;
%else %do;
%put It was equal to or lower =( ;
%end;
%mend;
%check_dates;
A few comments on the final macro... The macro variable named true has been replaced with a more descriptive variable named check_start_compared_higher. Because we just need a boolean value stored in it, we can simply assigning it the result of evaluating the expression &check_start > &compare_date which will return either a 0 (if false) or a 1 (if true). This is easier to read than using %if...%else... statements to do an assignment, as it is immediately clear that the line of code is simply performing an assignment and nothing more.
The line %if &check_start_compared_higher %then %do; shows how we can use the newly saved value to control program flow. Because the value in &check_start_compared_higher resolves to either TRUE or FALSE we can easily use it this way to make easy-to-read if-statements.
I am writing a macro variable that aims at producing a valid SQL WHERE clause as specified by user in prompts. Assume we have 3 variables X, Y, Z, and so on. User may specify the filter for every variable and macro variable looks like:
a = x eq 1 and y eq 1 and z eq 1;
which is then proceeded to the WHERE clause.
But if user specifies only, let's say' filter for Y it looks like:
a = and y eq 1 and
And I would like it to look like:
a = y eq 1
That is why I would like to somehow remove the operand 'AND' when it starts or ends the expression (it may start or end it multiple times, e.g. if we fitler only Z variable it looks like):
a = and and and z eq 1
I suppose it could be easily done with regular expressions but since I'm new to it, is there anyone willing to help me ? ;)
Slight rework of #DirkHorsten's technique. This simply organizes the code in a slightly different manner so that the SQL statement can be more easily read. In my opinion, the SQL statement is the important piece of code that you would like readers to understand (so let's keep it simple!), while the building of the where clause is just a side-note. This can be a valuable approach, especially as your SQL statements become more complex.
Approach 1, a single variable for all filters:
%macro getMyData(xValue=, yValue=, zValue=);
%local and_filters;
* THE BORING IMPLEMENTATION DETAILS ARE KEPT SEPARATE;
%let and_filters = ;
%if "&xValue" ne "" %then %do;
%let and_filters = &and_filters and sex eq "&xValue";
%end;
%if "&yValue" ne "" %then %do;
%let and_filters = &and_filters and age eq &yValue;
%end;
%if "&zValue" ne "" %then %do;
%let and_filters = &and_filters and height eq &zValue;
%end;
* THE IMPORTANT PIECE OF CODE IS EASY TO READ;
proc sql;
select *
from sashelp.class
where 1 &and_filters
;
quit;
%mend;
Approach 2, individual variables for each filter:
%macro getMyData(xValue=, yValue=, zValue=);
%local and_sex_filter and_age_filter and_height_filter;
* THE BORING IMPLEMENTATION DETAILS ARE KEPT SEPARATE;
%let and_sex_filter = ;
%let and_age_filter = ;
%let and_height_filter = ;
%if "&xValue" ne "" %then %do;
%let and_sex_filter = and sex eq "&xValue";
%end;
%if "&yValue" ne "" %then %do;
%let and_age_filter = and age eq &yValue;
%end;
%if "&zValue" ne "" %then %do;
%let and_height_filter = and height eq &zValue;
%end;
* THE IMPORTANT PIECE OF CODE IS EASY TO READ;
proc sql;
select *
from sashelp.class
where 1
&and_sex_filter
&and_age_filter
&and_height_filter
;
quit;
%mend;
Assuming you're doing this via macro parameters, this is easier to do by supplying a default.
%macro filter(x=1,y=1,z=1);
where &x. and &y. and &z.;
%mend filter;
1 is "true", so it acts (along with "AND") as a left-out argument.
If you only want to pass values (not the full equality), then you can also do this:
%macro filter(x=x, y=y, z=z);
where x=&x. and y=&y. and z=&z.;
%mend filter;
x=x is always true in SAS, but if you're passing through to SQL Server or Oracle and might have nulls this will not work (as null=null is false in SQL Server or Oracle).
%macro getMyData(xValue=, yValue=, zValue=);
proc sql;
select *
from sashelp.class
where
%if %length(&xValue) %then %do;
sex = "&xValue." and
%end;
%if %length(&yValue) %then %do;
age = &yValue. and
%end;
%if %length(&zValue) %then %do;
height >= &zValue. and
%end;
1;
quit;
%mend;
Title 'Females';
%getMyData(xValue=F);
Title '12 year';
%getMyData(yValue=12);
Title 'Large males';
%getMyData(xValue=M, zValue=60);
You can use a little known "where also" expression. Appended "where also" expressions logically equal to AND operator for each WHERE clause and you can user "where also" as your first WHERE clause without any issues to your code.
If you have a macro like that:
%MACRO get_data;
data want;
set have;
where a = x eq 1 and y eq 1 and z eq 1;
RUN;
%MEND;
You can rewrite to someting like:
%MACRO get_data;
data want;
set have;
%IF &X ne %THEN
%DO;
where also &x eq 1;
%END;
%IF &Y ne %THEN
%DO;
where also &y eq 1;
%END;
%IF &Z ne %THEN
%DO;
where also &z eq 1;
%END;
RUN;
%MEND;
Before you test the code, you need to at least initialise the macro variables.
You could do it with something like that:
%IF %symexist(&Z)=0 %THEN %LET Z = ;
Thanks all,
I have already figured out a similar structure as #Robert Penridge, but I appriciate all of the answers :) Thanks!
PS. I also was not familiar with WHERE ALSO - may prove useful in the future:)
data sample;
input x $;
datalines;
one
two
three
;
%macro variable_to_macvar(variable=, dataset=);
proc sql noprint;
select &variable into : outlist separated by ' '
from &dataset;
quit;
&outlist
%mend variable_to_macvar;
%put %variable_to_macvar(variable=x, dataset=sample);
Expected output: one two three. Instead I get an error. Why? Is this fixable?
I've successfully created other macros of a very similar form, where the function "returns" a value using the ¯ovariable at the end of the macro without a semicolon. For example, here is a similar type of function that works:
%macro zcat(first=5, last=15, prefix=freq);
%let x=;
%do i = &first %to &last;
%let x=&x &prefix.&i;
%end;
&x
%mend zcat;
%put %zcat();
You cannot execute a macro that involves running a proc or a data step in the way that you're trying to do here. You would need to use something like %sysfunc(dosubl(proc sql...)) in order for that to work (assuming you have SAS 9.3+ - see Joe's answer above). Otherwise, you can't use proc sql within a function-style macro.
More details about dosubl:
http://support.sas.com/documentation/cdl/en/lefunctionsref/67398/HTML/default/viewer.htm#p09dcftd1xxg1kn1brnjyc0q93yk.htm
It would be a bit fiddly, but if you really wanted to make this work as a function-style macro in earlier versions of SAS, you could construct it using the open, fetchobs and getvarc functions instead.
Update: Here's an example (using call set rather than getvarc, as this turned out to be simpler), in case anyone needs to do this in SAS 9.2 or earlier.
%macro variable_to_macvar(var,ds);
%local rc dsid i;
%let &var =;
%global outlist;
%let outlist=;
%let dsid = %sysfunc(open(&ds,i));
%syscall set(dsid);
%let rc = 0;
%let i = 0;
%do %while(&rc = 0);
%let i = %eval(&i + 1);
%let rc = %sysfunc(fetchobs(&dsid,&i));
%if &rc = 0 %then %let outlist = &outlist &&&var;
%end;
%let rc = %sysfunc(close(&dsid));
&outlist
%mend;
%put %variable_to_macvar(var=x, ds=sample);
Now works for views as well as ordinary datasets.
DOSUBL is available (but experimental) in 9.3 (at least, 9.3TS1M2, which I have). This is how you'd do it.
data sample;
input x $;
datalines;
one
two
three
;
%macro variable_to_macvar(variable=, dataset=);
%let rc=%sysfunc(dosubl(%str(
proc sql noprint;
select &variable into : outlist separated by ' '
from &dataset;
quit;
)));
&outlist
%mend variable_to_macvar;
%put %variable_to_macvar(variable=x, dataset=sample);;
If you can't use DOSUBL, or want to avoid experimental things, you can do this with PROC FCMP rather than a macro. If you like to write functions, PROC FCMP is probably for you: actually being able to write functions, rather than having to deal with the annoyances of the macro language.
Alter your code at the end to
%global outlist;
%variable_to_macvar(variable=x, dataset=sample);
%put &outlist;
The %put wants to resolve only a macro variable or a single value. It cannot call a procedure. So call your macro and then print the result.
Also, delete the &outlist from the macro definition. Sorry I missed that initially.
EDIT: Alternative.
Change your macro definition to
%macro variable_to_macvar(variable=, dataset=);
proc sql noprint;
select &variable into : outlist separated by ' '
from &dataset;
quit;
%put &outlist
%mend variable_to_macvar;
Just do the %put inside the macro.
%variable_to_macvar(variable=x, dataset=sample);
will print the string to the log.
We have a utility macro that is probably one of our most used pieces of code that does this for us. It is similar to the code that #user667489 provided but includes some nice features including error catching, allows both character and numeric vars, allows you to specify seperators, quotes, quote characters, filters to the dataset, etc....
We just put this macro in our autocall library so that it's avaialble to all of our programs. Some examples of running the macro:
Example 1 - Default behaviour:
%put %variable_to_macvar(var=x, ds=samplex);
Result 1:
one,two,three
Not quite the desired output as the default seperator is a comma, this is easily changed though...
Example 2 - Specify to use a space character as a delimiter:
%put %ds2list(iDs=samplex, iField=x, iDelimiter=%str( ));
Result 2:
one two three
Example 3 - Quoting & example usage
data names;
input name $;
datalines;
John
Jim
Frankie
;
run;
%put %ds2list(iDs=names, iField=name, iQuote=1);
proc sql noprint;
create table xx as
select *
from sashelp.class
where name in (%ds2list(iDs=names, iField=name, iQuote=1))
;
quit;
Result 3:
The below is printed to the log:
'John','Jim','Frankie'
Notice how we don't need to even save the result to a macro variable to use it in the SQL statement! Swweeet! This works just as well for SQL passthrough queries, and any other data step or proc statement that you can throw it at. In the above example, a single row is returned as 'John' is the only match found...
Anyway, that's our solution here... been using this for >10 years and works well for me. Here is the macro:
/***************************************************************************
** PROGRAM: MACRO.DS2LIST.SAS
**
** UTILITY PROGRAM THAT DETECTS RETURNS A LIST OF FIELD VALUES FROM A
** DATASET IN DELIMITED FORMAT.
**
** PARAMETERS:
** iDs : THE LIBNAME.DATASET NAME THAT YOU WANT TO CHECK.
** iField : THE FIELD THAT CONTAINS THE VALUES YOU WANT RETURNED IN A
** DELIMITED FORMAT.
** iDelimiter: DEFAULT IS A COMMA. THE DELIMITER TO USE FOR THE RETURNED LIST.
** iDsOptions: ANY STANDARD DATASET OPTIONS THAT YOU WOULD LIKE TO APPLY SUCH
** AS A WHERE STATEMENT.
** iQuote : (0=NO,1=YES). DEFAULT=0/NO. DETERMINES WHETHER THE RETURNED
** LIST IS QUOTED OR NOT.
** iQuoteChar: (SINGLE,DOUBLE) DEFAULT=SINGLE. SPECIFIES WHETHER SINGLE
** OR DOUBLE QUOTES ARE USED WHEN QUOTING THE RETURNED LIST
**
*****************************************************************************
** VERSION:
** 1.8 MODIFIED: 11-OCT-2010 BY: KN
** ALLOW BLANK CHARACTER VALUES AND ALSO REMOVED TRAILING
** ALLOW PARENTHESES IN CHARACTER VALUES
*****************************************************************************/
%macro ds2list(iDs=, iField=, iDsOptions=, iDelimiter=%str(,), iQuote=0, iQuoteChar=single);
%local dsid pos rc result cnt quotechar;
%let result=;
%let cnt=0;
%if &iQuote %then %do;
%if "%upcase(&iQuoteChar)" eq "DOUBLE" %then %do;
%let quotechar = %nrstr(%");
%end;
%else %if "%upcase(&iQuoteChar)" eq "SINGLE" %then %do;
%let quotechar = %nrstr(%');
%end;
%else %do;
%let quotechar = %nrstr(%");
%put WARNING: MACRO.DS2LIST.SAS: PARAMETER IQUOTECHAR INCORRECT. DEFAULTED TO DOUBLE;
%end;
%end;
%else %do;
%let quotechar = ;
%end;
/*
** ENSURE ALL THE REQUIRED PARAMETERS WERE PASSED IN.
*/
%if "&iDs" ne "" and "&iField" ne "" %then %do;
%let dsid=%sysfunc(open(&iDs(&iDsOptions),i));
%if &dsid %then %do;
%let pos=%sysfunc(varnum(&dsid,&iField));
%if &pos %then %do;
%let rc=%sysfunc(fetch(&dsid));
%do %while (&rc eq 0);
%if "%sysfunc(vartype(&dsid,&pos))" = "C" %then %do;
%let value = %qsysfunc(getvarc(&dsid,&pos));
%if "%trim(&value)" ne "" %then %do;
%let value = %qsysfunc(cats(%nrstr(&value)));
%end;
%end;
%else %do;
%let value = %sysfunc(getvarn(&dsid,&pos));
%end;
/* WHITESPACE/CARRIAGE RETURNS REMOVED IN THE BELOW LINE */
/* TO ENSURE NO WHITESPACE IS RETURNED IN THE OUTPUT. */
%if &cnt ne 0 %then %do;%unquote(&iDelimiter)%end;%unquote("echar&value"echar.)
%let cnt = %eval(&cnt + 1);
%let rc = %sysfunc(fetch(&dsid));
%end;
%if &rc ne -1 %then %do;
%put WARNING: MACRO.DS2LIST.SAS: %sysfunc(sysmsg());
%end;
%end;
%else %do;
%put ERROR: MACRO.DS2LIST.SAS: FIELD &iField NOT FOUND IN DATASET %upcase(&iDs).;
%end;
%end;
%else %do;
%put ERROR: MACRO.DS2LIST.SAS: DATASET %upcase(&iDs) COULD NOT BE OPENED.;
%end;
%let rc=%sysfunc(close(&dsid));
%end;
%else %do;
%put ERROR: MACRO.DS2LIST.SAS: YOU MUST SPECIFY BOTH THE IDS AND IFIELD PARAMETERS TO CALL THIS MACRO.;
%end;
%mend;