I am trying to compare two numberic value in a Macro.
But I keep getting the following message:
ERROR: A character operand was found in the %EVAL function or %IF condition where a numeric operand is required. The condition was: 0.2
ERROR: The %TO value of the %DO I loop is invalid.
ERROR: A character operand was found in the %EVAL function or %IF condition where a numeric operand is required. The condition was: 0.05
ERROR: The %BY value of the %DO I loop is invalid.
ERROR: The macro FAIL will stop executing.
My code is the following:
%macro fail;
%do i=0 %to 0.2 %by 0.05;
data failcrs;
set fail;
if f_p>=input(&i, 8.) then output;
run;
%end;
%mend failcrs;
f_p is a numeric variable.
What is wrong with my code? Please help.
Thank you so much!
Conditional tests in macro code (%if, %until, %while, etc) use %eval() macro function that only does integer arithmetic. This includes the increments and tests done in a %do-%to-%by loop.
To use floating point arithmetic you need to use the %sysvalf() macro function.
You could code your own increments to the loop counter.
%let i=0;
%do %while( %sysevalf( &I <= 0.2 ) );
...
%let i=%sysevalf(&i + 0.05);
%end;
Or make the loop counter an integer and use another macro variable to hold the fraction.
%do j=0 %to 20 %by 5 ;
%let i=%sysevalf(&j/100);
...
%end;
You have a couple of issues.
Macro loops work better with integers, but an easy workaround is a %DO %UNTIL loop instead.
Name on %MEND is different than on %MACRO
Invalid values for %DO %I loop.
Non-unique data set name, which means the output overwrites itself.
*fake data to work with;
data fail;
do f_p=0 to 0.2 by 0.01;
output;
end;
run;
%macro fail;
%let i=0;
%do %until(&i = 0.2); /*2*/
data failcrs_%sysevalf(&i*100); /*3*/
set fail;
if f_p>=&i then output;
run;
%let i = %sysevalf(&i + 0.05);
%end;
%mend fail; /*3*/
*test macro;
%fail;
The numbers in the comments align with the issues identified.
Try using best32. But why do you want to loop, when your dataset is overwritten for each loop. Please check log for each of step below. As at #Reeza in comments explains below you even do not an input statement
Options mprint;
/* do this*/
%macro fail;
%let i =15;
data failcrs;
set sashelp.class;
if age lt input(&i, best32.) then output;
run;
%mend fail;
%fail
/* dataset overwritten every time to finally pick up 15 as value check in the log*/
%macro fail1;
%do i = 1 %to 15;
data failcrs1;
set sashelp.class;
if age lt input(i, best32.) then output;
run;
%end;
%mend fail1;
%fail1
%macro fail;
%let i=0;
%do %until(&i = 0.2);
data failcrs;
set crse_grade_dist_fail;
if f_p>=&i then output;
run;
proc sql;
create table count_failclass as
select strm, count(class_nbr) as numfclass_%sysevalf(&i*100)
from failcrs
group by strm;
quit;
proc sql;
create table failfaculty as
select strm, instructor_id, instructor, count(class_nbr) as numfclass
from failcrs
group by strm, instructor_id, instructor;
quit;
proc sql;
create table count_failfaculty as
select strm, count(instructor) as numffaculty_%sysevalf(&i*100)
from failfaculty
group by strm;
quit;
data count_class_faculty;
set count_class_faculty;
set count_failclass;
set count_failfaculty;
run;
%let i = %sysevalf(&i + 0.05);
%end;
%mend fail;
Good thing is my data doesn't have f_p=0, all of them is greater than zero. Because I only want to count failed courses.
Documentation is written to be read, a simple search for "SAS 9.4 Macro Do" should explain it all -- start, stop and by are integers -- integers in the sense that whatever macro source expression in their place evaluates implicitly or explicitly to an integer at need time.
The macro you coded is a little strange. It will generate multiple data steps that all overwrite the same dataset. You might want to concentrate on not writing macro code first, and move to it when the need to have repetitive boilerplate code submitted. Writing good macro code means you have to think in terms of "will this generate appropriate source code and what side effect will these macro statements have in their resolution scope"
%DO, Iterative Statement
Syntax
%DO macro-variable=start %TO stop <%BY increment>;
text and macro language statements
%END;
Required Arguments
macro-variable
names a macro variable or a text expression that generates a macro
variable name. Its value functions as an index that determines the
number of times the %DO loop iterates. If the macro variable specified
as the index does not exist, the macro processor creates it in the
local symbol table.
You can change the value of the
index variable during processing. For example, using conditional
processing to set the value of the index variable beyond the stop
value when a certain condition is met ends processing of the loop.
startstop
specify integers or macro expressions that generate integers
to control the number of times the portion of the macro between the
iterative %DO and %END statements is processed.
The first time the
%DO group iterates, macro-variable is equal to start. As processing
continues, the value of macro-variable changes by the value of
increment until the value of macro-variable is outside the range of
integers included by start and stop.
increment
specifies an integer
(other than 0) or a macro expression that generates an integer to be
added to the value of the index variable in each iteration of the
loop. By default, increment is 1. Increment is evaluated before the
first iteration of the loop. Therefore, you cannot change it as the
loop iterates.
Related
So I have a range of datasets in a specific library. These datasets are named in the format DATASET_YYYYMM, with one dataset for each month. I am trying to append a range of these datasets based on user input for the date range. i.e. If start_date is 01NOV2019 and the end_date is 31JAN2020, I want to append the three datasets: LIBRARY.DATASET_201911, LIBRARY.DATASET_201912 and LIBRARY.DATASET_202001.
The range is obviously variable, so I can't simply name the datasets manually in a set function. Since I need to loop through the years and months in the date range, I believe a macro is the best way to do this. I'm using a loop within the SET statement to append all the datasets. I have copied my example code below. It does work in theory. But in practice, only if we are looping over the months of November and December. As the format of the dataset name has a two digit month, for Jan-Sept it will be 01-09. The month function returns 1-9 however, and of course a 'File DATASET_NAME does not exist' error is thrown.
Problem is I cannot figure out a way to get it to interpret the month with leading 0, without ruining functionality of another part of the loop/macro.
I have tried numerous approaches to format the number as z2, cannot get any to work.
i.e. Including PUTN functions in the DO line for quote_month as follows, it ignores the leading zero when generating the dataset name in the line below.
%DO quote_month = %SYSFUNC(IFN("e_year. = &start_year.,%SYSFUNC(PUTN(&start_month.,z2.)),1,.)) %TO %SYSFUNC(IFN("e_year. = &end_year.,%SYSFUNC(PUTN(&end_month.,z2.)),12,.));
Below is example code (without any attempt to reformat it to z2) - it will throw an error because it cannot find 'dataset_20201' because it is actually called 'dataset_202001'. The dataset called dataset_combined_example produces the desired output of the code by manually referencing the dataset names which it will be unable to do in practice. Does anyone know how to go about this?
DATA _NULL_;
FORMAT start_date end_date DATE9.;
start_date = '01NOV2019'd;
end_date = '31JAN2020'd;
CALL symput('start_date',start_date);
CALL symput('end_date',end_date);
RUN;
DATA dataset_201911;
input name $;
datalines;
Nov1
Nov2
;
RUN;
DATA dataset_201912;
input name $;
datalines;
Dec1
Dec2
;
RUN;
DATA dataset_202001;
input name $;
datalines;
Jan1
Jan2
;
RUN;
DATA dataset_combined_example;
SET dataset_201911 dataset_201912 dataset_202001;
RUN;
%MACRO get_table(start_date, end_date);
%LET start_year = %SYSFUNC(year(&start_date.));
%LET end_year = %SYSFUNC(year(&end_date.));
%LET start_month = %SYSFUNC(month(&start_date.));
%LET end_month = %SYSFUNC(month(&end_date.));
DATA dataset_combined;
SET
%DO quote_year = &start_year. %TO &end_year.;
%DO quote_month = %SYSFUNC(IFN("e_year. = &start_year.,&start_month.,1,.)) %TO %SYSFUNC(IFN("e_year. = &end_year.,&end_month.,12,.));
dataset_"e_year."e_month.
%END;
%END;
;
RUN;
%MEND;
%get_table(&start_date.,&end_date.);
You could do this using putn and z2. format.
%DO quote_year = &start_year. %TO &end_year.;
%DO quote_month = %SYSFUNC(IFN("e_year. = &start_year.,&start_month.,1,.)) %TO %SYSFUNC(IFN("e_year. = &end_year.,&end_month.,12,.));
dataset_"e_year.%sysfunc(putn("e_month.,z2.))
%END;
%END;
You can also do this using the metadata tables without having to resort to macro loops in the first place:
/* A few datasets to combine */
data
DATASET_201910
DATASET_201911
DATASET_201912
DATASET_202001
;
run;
%let START_DATE = '01dec2019'd;
%let END_DATE = '31jan2020'd;
proc sql noprint;
select catx('.', libname, memname) into :DS_LIST separated by ' '
from dictionary.tables
where
&START_DATE <=
case
when prxmatch('/DATASET_\d{6}/', memname)
then input(scan(memname, -1, '_'), yymmn6.)
else -99999
end
<= &END_DATE
and libname = 'WORK'
;
quit;
data combined_datasets /view=combined_datasets;
set &DS_LIST;
run;
The case-when in the where clause ensures that any other datasets present in the same library that don't match the expected naming scheme are ignored.
One key difference with this approach is that you will never end up attempting to read a dataset that doesn't exist if one of the expected datasets in your range is missing.
You can use the Z format to generate strings with leading zeros.
But your problem is much easier if you use SAS date functions and formats to generate the YYYYMM strings. Just use a normal iterative %DO loop to cycle the month offset from zero to the number of months between the two dates.
%macro get_table(start_date, end_date);
%local offset dsname ;
data dataset_combined;
set
%do offset=0 %to %sysfunc(intck(month,&start_date,&end_date));
%let dsname=dataset_%sysfunc(intnx(month,&start_date,&offset),yymmn6);
&dsname.
%end;
;
run;
%mend get_table;
Result:
445 options mprint;
446 %get_table(start_date='01NOV2019'd,end_date='31JAN2020'd);
MPRINT(GET_TABLE): data dataset_combined;
MPRINT(GET_TABLE): set dataset_201911 dataset_201912 dataset_202001 ;
MPRINT(GET_TABLE): run;
In a macro
Use INTNX to compute the bounds for a loop over date values. Within the loop:
Compute the candidate data set name according to specified lib, prefix and desired date value format. <yyyy><mm> is output by format yymmn6.
Use EXIST to check candidate data sets for existence.
Alternatively, do not check, but make sure to set OPTIONS NODSNFERR prior to combining. The setting will prevent errors when specifying a non-existent data set.
Update the loop index to the end of the month so the next increment takes the index to the start of the next month.
%macro names_by_month(lib=work, prefix=data_, start_date=today(), end_date=today(), format=yymmn6.);
%local index name;
%* loop over first-of-the-month date values;
%do index = %sysfunc(intnx(month, &start_date, 0)) %to %sysfunc(intnx(month, &end_date, 0));
%* compute month dependent name;
%let name = &lib..&prefix.%sysfunc(putn(&index,&format));
%* emit name if it exists;
%if %sysfunc(exist(&name)) or %sysfunc(exist(&name,VIEW)) %then %str(&name);
%* prepare index for loop +1 increment so it goes to start of next month;
%let index = %sysfunc(intnx(month, &index, 0, E));
%end;
%mend;
* example usage:
data combined_imports(label="nov2019 to jan2020");
set
%names_by_month(
prefix=import_,
start_date='01NOV2019'd,
end_date = '31JAN2020'd
)
;
run;
I'm new to SAS Macro programming and need to enable the following macro to be able to handle and process multiple values for its macro parameters.Hello,
data have;
input name $ ACCOUNT_ID $ cust_id;
cards;
ARTHUR CC1234 1234
TOM eil1235 1235
MIKEZ tb1236 1236
MATT mb1237 1237
LIZ TB1238 1238
PIZ VB1239 1239
TAN MB1240 1240
PANDA . 1241
;
run;
%MACRO algo (IN_DS=,VAR_LIST=,DATA_TYPE_LIST=,OUT_DS=);
DATA &OUT_DS;
SET &IN_DS;
%If &data_type_LIST = num %then
&var_LIST=sum(&VAR_LIST,2);
%else &var_LIST=cats(&var_LIST,'re');;
run;
%mend;
%algo(IN_DS=HAVE,VAR_LIST=CUST_ID,DATA_TYPE_LIST=num,OUT_DS=out1);`
I now need to enable this macro to be able to pass multiple values for the macro parameters. Something like this :
%algo(IN_DS=HAVE,VAR_LIST='CUST_ID,ACCT_ID',DATA_TYPE_LIST='num,char',OUT_DS=out1);
Can someone help me enable this functionality in the macro code.
The parameter argument should be macro quoted with %STR() in the macro invocation.
Try
%algo
( IN_DS=HAVE
, VAR_LIST= %STR (CUST_ID, ACCT_ID)
, DATA_TYPE_LIST=num
, OUT_DS=out1
);
Macro quoting is different than DATA step quoting used for character literals.
Make sure the macro can handle multiple values. In general it is not a good idea to use comma as the delimiter in your list of values when calling a macro.
Usually space is the best delimiter since then you can use the macro value directly in the generated code. For example if your variables are all of the same type you can just use data step ARRAY.
%MACRO algo (IN_DS=,VAR_LIST=,DATA_TYPE_LIST=,OUT_DS=);
DATA &OUT_DS;
SET &IN_DS;
array list &var_list ;
do _n_=1 to dim(list);
%if &data_type_LIST = num %then %do ;
list(_n_)=sum(list(_n_),2);
%end;
%else %do;
list(_n_)=cats(list(_n_),'re');
%end;
end;
run;
%mend algo;
If your variables are NOT all of the same type then you need to generate a separate statement for each variable. In that case you can use a different delimiter if you want, like a pipe character, that is easier to use as delimiter in calls to macro functions like %scan().
%MACRO algo (IN_DS=,VAR_LIST=,DATA_TYPE_LIST=,OUT_DS=);
%local i var;
DATA &OUT_DS;
SET &IN_DS;
%do i=1 %to %sysfunc(countw(&var_list,|));
%let var=%scan(&var_list,&i,|);
%if %scan(&data_type_LIST,&i,|) = num %then %do ;
&var=sum(&var,2);
%end;
%else %do;
&var=cats(&var,'re');
%end;
%end;
run;
%mend algo;
%algo(IN_DS=HAVE,VAR_LIST=CUST_ID|ACCT_ID,DATA_TYPE_LIST=num|char,OUT_DS=out1);
If you want to pass a list of variables and then use that list in the code you posted above, my suggestion would be to treat the &var_list as a list, and use scan to determine how many variables there are, and then loop through the list and execute the code accordingly.
I am looking to have a programme which cleans up some messy data I have, I am looking to do this for both the assets and liabilities side of the project i'm working on.
My question is there a way to use a do loop to use the cleaning up data to first clean up the assets then liabilities. something like this:
%do %I = Asset %to Liability;
%assetorliability= I ;
proc sort data = &assetorliability;
by price;
run;
data want&assetorliability;
set &assetorliability;
if _N_ < 50000;
run;
the actual script is quite long so a singular macro may not be the ideal solution but this loop would be great.
TIA.
EDIT : the programme includes some macros and the errors received are as follows:
%let list =Asset Liability;
%do i=1 %to %sysfunc(countw(&list,%str( )));
%let next=%scan(&list,&i,%str( ));
%Balance;
%end;
in the macro the data steps are named with a balance&list to allow for each scenario. the errors are:
13221 %let list =Asset Liability;
13222 %do i=1 %to %sysfunc(countw(&list,%str( )));
ERROR: The %DO statement is not valid in open code.
13223
13224 %let next=%scan(&list,&i,%str( ));
WARNING: Apparent symbolic reference I not resolved.
WARNING: Apparent symbolic reference I not resolved.
ERROR: A character operand was found in the %EVAL function or %IF condition where a numeric
operand is required. The condition was: &i
ERROR: Argument 2 to macro function %SCAN is not a number.
ERROR: The %END statement is not valid in open code.
The macro %do statement is not as flexible as the data step do statement. To loop over a list of values you would want to put the list into a macro variable and use an index variable in your %do loop.
Note that macro logic needs to be inside of a macro. You cannot use it in "open" code.
%macro do_over(list);
%local i next;
%do i=1 %to %sysfunc(countw(&list,%str( )));
%let next=%scan(&list,&i,%str( ));
proc sort data = &next ;
by price;
run;
data want&next ;
...
%end;
%mend do_over ;
%do_over(Asset Liability)
I am trying to rename the variables based on one array elements in the folloing way,
%let var= class name gender;
data want;
set have;
%global noof;
array point(*)$ %str(&var) ;
a=dim(point);
call symputx('noof',a);
array newvar(&noof);
do i=1 to &noof;
newvar(i)=translate(point(i),',','.');
end;
drop &var;
do i=1 to &noof;
rename newvar(i)=vname(point(i));
end;
run;
I want to rename the new variables to the first array elemets.
LOG:
rename newvar(i)=vname(point(i));
-
22
76
ERROR 22-322: Syntax error, expecting one of the following: -, :, =.
ERROR 76-322: Syntax error, statement will be ignored.
Unfortunately, the value on the RHS of the RENAME statement must be a literal. The statement is evaluated at compile time, not run time.
Try this:
%let var= class name gender;
%macro translate(datain,dataout,vars);
%local n i var;
%let n=%sysfunc(countw(&vars));
data &dataout(rename=(
%do i=1 %to &n;
%let var = %scan(&vars,&i);
newvar&i = &var
%end;
));
set &datain;
array invars(&n) $ &vars ;
array newvar(&n) $;
do i=1 to &n;
newvar(i)=translate(invars(i),',','.');
end;
drop &vars i;
run;
%mend;
data test;
class = "1,2,3";
name= "Dom,Pazzula";
gender="M";
run;
%translate(test,out,&var);
You can run into issues if the length of these character variables are too large. The new variables might be truncated. You will have to modify this to add a length statement.
I am trying to change the length of the variables based on a list that I have and the code seems to work but the desired output is not achieved. here is the code:
%macro LEN();
Proc sql ;
select count(name) into: varnum from variab;
select name into: varname1-:varname%trim(%left(&varnum)) from Variab;
select length3 into: len from Length;
Quit;
%do i=1 %to &varnum;
data Zero;
length &&varname&i $ &&len&i.;
set desti.test;
length _numeric_ 4.;
format _numeric_ 12.2;
run;
%end;
%mend;
It gives a warning
WARNING: Multiple lengths were specified for the variable fscadl1 by
input data set(s). This can cause truncation
of data.
and it doesnt change the length of the variable. what is wrong in this code?
Are you trying to change a list of variables in one dataset? You're repeating the entire data step for each iteration, but only writing to a constant destination, which is inconsistent.
Probably what you want is:
Proc sql ;
select count(name) into: varnum from variab;
select name into: varname1-:varname%trim(%left(&varnum)) from Variab;
select length3 into: len from Length;
Quit;
%macro set_len(varnum=);
%do i=1 %to &varnum;
length &&varname&i $ &&len&i.;
%end;
%mend;
data Zero;
%set_len(&varnum);
set desti.test;
length _numeric_ 4.;
format _numeric_ 12.2;
run;
Note that you'd need to define &&len&i as you're not doing that currently.
The warning messages suggests that it is working. SAS started throwing that warning when you truncate a variable. You can suppress the warning message with the VARLENCHK option.
Below works:
options varlenchk=nowarn;
data want;
length name $ 3;
set sashelp.class;
length _numeric_ 4;
run;
If your code isn't working, I would turn on MPRINT to see make sure your macro is generating the SAS code you expect.