Files
dc/sas/sasjs/macros/mpe_derived.sas
T
dc 4d9ad38df1
Build / Build-and-ng-test (pull_request) Successful in 5m47s
Lighthouse Checks / lighthouse (pull_request) Successful in 20m49s
Build / Build-and-test-development (pull_request) Successful in 32m2s
fix(derived): verify on stripped values, and label with the variable name
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.
2026-10-20 11:51:58 +00:00

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;