Found running the macro on a real Viya compute server: - verify() was applied to fixed width variables, so the blank padding was always outside the allowed character list and every name was rejected. The check now runs on strip(name) and strip(fmt). - the empty outvars dataset used 'length name $32; stop;', which the Viya compute server rejects with 'Variable name is uninitialized'. It now assigns the variable and subsets with 'if 0;'. - the label is now <VAR_NM>=<expression> rather than 'DERIVED: <expression>', so the grid shows the variable name as well as the logic.
287 lines
8.6 KiB
SAS
287 lines
8.6 KiB
SAS
/**
|
|
@file
|
|
@brief Generates the SAS statements that create derived variables
|
|
@details Derived (computed) variables are configured in MPE_DERIVED. Each
|
|
row defines one variable, which is created in a DATA step view ahead of the
|
|
filter, so that it can be filtered on in the EDIT screen.
|
|
|
|
The macro writes the generated statements to a fileref, so that callers can
|
|
compose them into a view:
|
|
|
|
%mpe_derived(libds=&libds,dclib=&mpelib,outref=dercode)
|
|
data work.vw_derived / view=work.vw_derived;
|
|
set &libds;
|
|
%include dercode;
|
|
run;
|
|
|
|
Alternatively, pass outview= and the macro will build the view itself.
|
|
|
|
The filter must be applied by the caller, in the step that reads the view:
|
|
|
|
data out;
|
|
set work.vw_derived;
|
|
where %inc filtref;;
|
|
run;
|
|
|
|
A WHERE statement applies to the data set named on the SET statement, so it
|
|
cannot reference a variable that is created in the same data step. Applying
|
|
it to the view puts the derived variables on the input, which is what makes
|
|
them filterable.
|
|
|
|
Each statement block is of the form:
|
|
|
|
attrib REGION length=$21 label='REGION=<expression>' format=$21.;
|
|
REGION = <expression>;
|
|
|
|
Type and length are declared by the administrator, so the result is
|
|
deterministic. The label is the variable name, an equals sign and the
|
|
expression, so the grid shows both what the column is and where it comes
|
|
from.
|
|
|
|
Statements are generated in VAR_ORDER sequence, so a derived variable
|
|
may reference another derived variable with a lower order. A reference to a
|
|
variable that is not defined earlier is rejected, because SAS would otherwise
|
|
treat it as an uninitialised variable and return a missing value without an
|
|
error.
|
|
|
|
Semicolons, macro triggers and comments are rejected, so a row can only ever
|
|
be an expression.
|
|
|
|
<h4> SAS Macros </h4>
|
|
@li mf_existds.sas
|
|
@li mf_getvarlist.sas
|
|
@li mf_nobs.sas
|
|
@li mp_abort.sas
|
|
|
|
@param [in] libds= The target table (libref.dataset, lib must be assigned)
|
|
@param [in] dclib= The Data Controller control library
|
|
@param [out] outref= (0) Fileref to receive the generated statements
|
|
@param [out] outvars= (work.derived_vars) Dataset of derived variable names
|
|
@param [in] outview= (0) Optional two-level name for the derived view
|
|
|
|
@version 9.2
|
|
@author 4GL Apps Ltd
|
|
@copyright 4GL Apps Ltd. This code may only be used within Data Controller
|
|
and may not be re-distributed or re-sold without the express permission of
|
|
4GL Apps Ltd.
|
|
|
|
**/
|
|
|
|
%macro mpe_derived(libds=,
|
|
dclib=,
|
|
outref=0,
|
|
outvars=work.derived_vars,
|
|
outview=0
|
|
);
|
|
|
|
%local physlist derived_err;
|
|
|
|
%mp_abort(iftrue= (%length(&libds)=0 or %length(&dclib)=0)
|
|
,mac=&sysmacroname
|
|
,msg=%str(Missing: libds dclib)
|
|
)
|
|
%mp_abort(iftrue= (&outref = 0)
|
|
,mac=&sysmacroname
|
|
,msg=%str(Please provide an outref for the generated statements)
|
|
)
|
|
|
|
filename &outref temp;
|
|
|
|
/* ensure the outputs exist, and are empty */
|
|
data _null_;
|
|
file &outref;
|
|
run;
|
|
data &outvars;
|
|
length name $32;
|
|
if 0 then name='';
|
|
run;
|
|
|
|
%if %mf_existds(libds=&dclib..mpe_derived) ne 1 %then %do;
|
|
%put &sysmacroname: &dclib..MPE_DERIVED not found - no derived variables;
|
|
%return;
|
|
%end;
|
|
|
|
data work.derived_cfg;
|
|
set &dclib..mpe_derived;
|
|
where &dc_dttmtfmt lt tx_to
|
|
and active=1
|
|
and upcase(libref)="%upcase(%scan(&libds,1,.))"
|
|
and upcase(dsn)="%upcase(%scan(&libds,2,.))";
|
|
length name $32 logic $4000 fmt $32;
|
|
name=upcase(var_nm);
|
|
logic=strip(var_logic);
|
|
type=upcase(var_type);
|
|
len=var_len;
|
|
fmt=strip(upcase(var_fmt));
|
|
ord=var_order;
|
|
run;
|
|
|
|
%if %mf_nobs(work.derived_cfg)=0 %then %return;
|
|
|
|
/* statement order is the configured order - a derived variable may reference
|
|
one with a lower VAR_ORDER, so they must be assigned in this sequence */
|
|
proc sort data=work.derived_cfg out=work.derived_srt;
|
|
by ord name;
|
|
run;
|
|
|
|
proc sort data=work.derived_cfg out=work.derived_byname;
|
|
by name;
|
|
run;
|
|
|
|
/**
|
|
* Reject anything that could break the generated data step, or that would
|
|
* produce a variable we cannot describe to the client
|
|
*/
|
|
%let physlist=%upcase(%mf_getvarlist(&libds,dlm=%str(,),quote=SINGLE));
|
|
|
|
data work.derived_bad;
|
|
set work.derived_srt;
|
|
length reason $200;
|
|
call missing(reason);
|
|
|
|
if name='' then reason='VAR_NM is required';
|
|
else if verify(strip(name),'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')>0
|
|
then reason='VAR_NM must be a valid SAS name';
|
|
else if verify(substr(name,1,1),'0123456789')=0
|
|
then reason='VAR_NM must not start with a digit';
|
|
else if name in (%unquote(&physlist))
|
|
then reason='VAR_NM collides with a column in the target table';
|
|
else if name in ('_____DELETE__THIS__RECORD_____','TX_FROM','TX_TO'
|
|
,'PROCESSED_DTTM','VAR_TXFROM','VAR_TXTO','VAR_PROCESSED')
|
|
then reason='VAR_NM is reserved';
|
|
else if missing(ord) or ord ne int(ord) or ord<1
|
|
then reason='VAR_ORDER must be a positive integer';
|
|
else if type not in ('C','N')
|
|
then reason='VAR_TYPE must be C or N';
|
|
else if missing(len) or len ne int(len) or len<1
|
|
then reason='VAR_LEN must be a positive integer';
|
|
else if type='C' and len>32767
|
|
then reason='VAR_LEN exceeds 32767 for a character variable';
|
|
else if type='N' and len>8
|
|
then reason='VAR_LEN exceeds 8 for a numeric variable';
|
|
else if index(fmt,';')>0 or indexc(fmt,'&%')>0
|
|
or index(fmt,'/*')>0 or index(fmt,'*/')>0
|
|
then reason='VAR_FMT must be a format name, without ; & % or //*';
|
|
else if verify(strip(fmt),'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$.')>0
|
|
then reason='VAR_FMT must be a format name';
|
|
else if missing(logic)
|
|
then reason='VAR_LOGIC is required';
|
|
else if index(logic,';')>0
|
|
then reason='VAR_LOGIC must be a single expression - remove the ;';
|
|
else if indexc(logic,'&%')>0
|
|
then reason='VAR_LOGIC must not contain macro triggers';
|
|
else if index(logic,'/*')>0 or index(logic,'*/')>0
|
|
then reason='VAR_LOGIC must not contain comments';
|
|
|
|
if missing(reason)=0 then output;
|
|
run;
|
|
|
|
data work.derived_dup;
|
|
set work.derived_byname;
|
|
by name;
|
|
length reason $200;
|
|
if first.name and not last.name;
|
|
reason='VAR_NM is defined more than once';
|
|
run;
|
|
|
|
proc append base=work.derived_bad data=work.derived_dup;
|
|
run;
|
|
|
|
/* a derived variable may only reference derived variables defined earlier -
|
|
otherwise the reference silently resolves to a missing value */
|
|
proc sql noprint;
|
|
create table work.derived_orderbad_key as
|
|
select distinct a.name
|
|
from work.derived_srt a
|
|
inner join work.derived_srt b
|
|
on a.name ne b.name
|
|
where prxmatch(
|
|
cats('/(^|[^A-Z0-9_])',b.name,'([^A-Z0-9_]|$)/')
|
|
,upcase(a.logic)
|
|
)>0
|
|
and b.ord>=a.ord
|
|
order by a.name;
|
|
quit;
|
|
|
|
proc sort data=work.derived_orderbad_key;
|
|
by name;
|
|
run;
|
|
|
|
data work.derived_orderbad;
|
|
merge work.derived_byname(in=in1) work.derived_orderbad_key(in=in2);
|
|
by name;
|
|
if in2;
|
|
length reason $200;
|
|
reason='VAR_LOGIC references a derived variable that is not defined'
|
|
!!' earlier - lower the VAR_ORDER of this one';
|
|
run;
|
|
|
|
proc append base=work.derived_bad data=work.derived_orderbad;
|
|
run;
|
|
|
|
%if %mf_nobs(work.derived_bad)>0 %then %do;
|
|
data _null_;
|
|
set work.derived_bad;
|
|
putlog 'ERR' 'OR: derived variable config: ' name= reason=;
|
|
run;
|
|
data _null_;
|
|
set work.derived_bad(obs=1);
|
|
call symputx('derived_err',cats(name,': ',reason));
|
|
run;
|
|
%mp_abort(iftrue=1
|
|
,mac=&sysmacroname
|
|
,msg=%str(Invalid derived variable configuration - &derived_err)
|
|
)
|
|
%end;
|
|
|
|
/**
|
|
* Generate the attrib + assignment statements
|
|
*/
|
|
data _null_;
|
|
file &outref;
|
|
set work.derived_srt;
|
|
length stmt $4100 label $256 fmt2 $49;
|
|
/* labels cannot contain single quotes, and are limited to 256 chars */
|
|
label=cats(name,'=',translate(logic,' ','0D0A'x));
|
|
label=tranwrd(label,"'","''");
|
|
label=substr(label,1,256);
|
|
if not missing(fmt) and index(fmt,'.')=0 then fmt=cats(fmt,'.');
|
|
if type='C' then do;
|
|
if missing(fmt) then fmt2=cats('$',len,'.');
|
|
else fmt2=fmt;
|
|
stmt=catx(' ','attrib',name,cats('length=$',len)
|
|
,cats("label='",label,"'"),cats('format=',fmt2,';'));
|
|
end;
|
|
else do;
|
|
if missing(fmt) then fmt2=cats(len,'.');
|
|
else fmt2=fmt;
|
|
stmt=catx(' ','attrib',name,cats('length=',len)
|
|
,cats("label='",label,"'"),cats('format=',fmt2,';'));
|
|
end;
|
|
put stmt;
|
|
stmt=cats(name,' = ',logic,';');
|
|
put stmt;
|
|
run;
|
|
|
|
%mp_abort(iftrue= (&syscc ne 0)
|
|
,mac=&sysmacroname
|
|
,msg=%str(Unable to generate derived variables for &libds)
|
|
)
|
|
|
|
data &outvars(keep=name);
|
|
set work.derived_srt;
|
|
run;
|
|
|
|
%if %length(&outview)>0 and &outview ne 0 %then %do;
|
|
data &outview / view=&outview;
|
|
set &libds;
|
|
%include &outref;
|
|
run;
|
|
%mp_abort(iftrue= (&syscc ne 0)
|
|
,mac=&sysmacroname
|
|
,msg=%str(Unable to build derived view &outview for &libds)
|
|
)
|
|
%end;
|
|
|
|
%mend mpe_derived;
|