Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mp_derivedvars.test.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Testing mp_derivedvars macro
4
5 <h4> SAS Macros </h4>
6 @li mddl_derivedvars.sas
7 @li mf_existds.sas
8 @li mf_existvar.sas
9 @li mf_nobs.sas
10 @li mp_assert.sas
11 @li mp_assertcols.sas
12 @li mp_assertdsobs.sas
13 @li mp_assertscope.sas
14 @li mp_derivedvars.sas
15
16**/
17
18/* the target table */
19data work.class;
20 set sashelp.class;
21run;
22
23/* the control table */
24%mddl_derivedvars(libds=work.dvcfg)
25
26proc sql;
27insert into work.dvcfg
28 set libref='WORK',dsn='CLASS',var_nm='band',var_deps='AGE'
29 ,var_type='C',var_len=8,var_fmt='',active=1
30 ,var_logic="ifc(AGE>13,'TEEN','JUNIOR')";
31insert into work.dvcfg
32 set libref='WORK',dsn='CLASS',var_nm='band_age',var_deps='BAND AGE'
33 ,var_type='C',var_len=16,var_fmt='',active=1
34 ,var_logic="cats(BAND,'-',AGE)";
35insert into work.dvcfg
36 set libref='WORK',dsn='CLASS',var_nm='double_age',var_deps='AGE'
37 ,var_type='N',var_len=8,var_fmt='',active=1
38 ,var_logic='AGE * 2';
39/* loaded first, so the dependency order cannot come from the row order */
40insert into work.dvcfg
41 set libref='WORK',dsn='CLASS',var_nm='band_label',var_deps='BAND SEX'
42 ,var_type='C',var_len=32,var_fmt='',active=1
43 ,var_logic="cats(BAND,'-',SEX)";
44/* inactive - should be excluded by the where clause */
45insert into work.dvcfg
46 set libref='WORK',dsn='CLASS',var_nm='ignored',var_deps=''
47 ,var_type='C',var_len=8,var_fmt='',active=0
48 ,var_logic="'X'";
49quit;
50
51/* the setup SQL above creates the SQL automatic macro variables, so the
52 snapshot is taken here - the check is for the macro's own leakage */
53%mp_assertscope(SNAPSHOT)
54
55%mp_derivedvars(
56 libds=work.class,
57 control=work.dvcfg,
58 where=%str(active=1),
59 outref=derref,
60 outview=work.vw_dv,
61 outvars=work.dvnames,
62 reserved=%str(_____DELETE__THIS__RECORD_____ TX_FROM TX_TO)
63)
64
65/* one row per variable, names upcased, inactive row excluded */
66%mp_assertdsobs(work.dvnames,
67 desc=Only the active variables are returned,
68 test=EQUALS 4,
69 outds=work.test_results
70)
71
72%mp_assertcols(work.vw_dv,
73 desc=The view carries the derived columns,
74 cols=BAND BAND_AGE DOUBLE_AGE BAND_LABEL,
75 test=ALL,
76 outds=work.test_results
77)
78
79%mp_assert(
80 iftrue=(%mf_existvar(work.vw_dv,IGNORED)=0),
81 desc=The inactive variable is not in the view,
82 outds=work.test_results
83)
84
85/* the values, including a variable built on another one */
86data work.chk;
87 set work.vw_dv;
88 where band='TEEN';
89 if band_age ne cats(band,'-',AGE) then output;
90 if double_age ne AGE*2 then output;
91 if band_label ne cats(band,'-',SEX) then output;
92run;
93
94%mp_assertdsobs(work.chk,
95 desc=Derived values and chained values are correct,
96 test=EQUALS 0,
97 outds=work.test_results
98)
99
100data work.chk2;
101 set work.vw_dv;
102 where band='TEEN';
103run;
104
105%mp_assertdsobs(work.chk2,
106 desc=A derived variable can be filtered on,
107 test=HASOBS,
108 outds=work.test_results
109)
110
111/* the label is the variable name plus the expression */
112proc contents noprint data=work.vw_dv out=work.vcols(keep=name label);
113run;
114
115data work.chklab;
116 set work.vcols;
117 where name='BAND' and label=:'BAND=ifc(AGE>13';
118run;
119
120%mp_assertdsobs(work.chklab,
121 desc=The label carries the variable name and the expression,
122 test=EQUALS 1,
123 outds=work.test_results
124)
125
126%mp_assert(
127 iftrue=("%mf_existds(libds=work.vw_dv)"="1"),
128 desc=The view was built,
129 outds=work.test_results
130)
131
132/* the statements are generated in dependency order, not in the order the
133 rows were loaded - BAND must be assigned before BAND_AGE */
134data work.stmts;
135 infile derref truncover;
136 input line $4000.;
137 seq=_n_;
138run;
139
140data work.chkord;
141 set work.stmts end=eof;
142 retain band_seq bandage_seq;
143 if prxmatch('/^BAND\s*=/',strip(line)) and missing(band_seq) then band_seq=seq;
144 if prxmatch('/^BAND_AGE\s*=/',strip(line)) and missing(bandage_seq)
145 then bandage_seq=seq;
146 if eof;
147 if missing(band_seq) or missing(bandage_seq) or band_seq>=bandage_seq
148 then output;
149run;
150
151%mp_assertdsobs(work.chkord,
152 desc=BAND is assigned before BAND_AGE whatever order the rows were loaded,
153 test=EQUALS 0,
154 outds=work.test_results
155)
156
157/* VAR_LABEL overrides the default label, and falls back when it is blank */
158%mddl_derivedvars(libds=work.dvcfg_label)
159
160proc sql;
161insert into work.dvcfg_label
162 set libref='WORK',dsn='CLASS',var_nm='band',var_deps='AGE'
163 ,var_type='C',var_len=8,var_fmt='',var_label='Age band'
164 ,var_logic="ifc(AGE>13,'TEEN','JUNIOR')",active=1;
165insert into work.dvcfg_label
166 set libref='WORK',dsn='CLASS',var_nm='double_age',var_deps='AGE'
167 ,var_type='N',var_len=8,var_fmt='',var_label=''
168 ,var_logic='AGE * 2',active=1;
169quit;
170
171%mp_derivedvars(
172 libds=work.class,
173 control=work.dvcfg_label,
174 outref=derref7,
175 outview=work.vw_label
176)
177
178/* assert on the generated statements - the label in the view can be changed
179 again by proc contents, the statement is what the macro emits */
180data work.stmts7;
181 infile derref7 truncover;
182 input line $4000.;
183run;
184
185data work.chklbl;
186 set work.stmts7 end=eof;
187 retain found_custom found_default;
188 if index(line,"label='Age band'")>0 then found_custom=1;
189 if index(line,"label='DOUBLE_AGE=AGE * 2'")>0 then found_default=1;
190 if eof;
191 if not found_custom or not found_default then output;
192run;
193
194%mp_assertdsobs(work.chklbl,
195 desc=VAR_LABEL is used when set and defaults when blank,
196 test=EQUALS 0,
197 outds=work.test_results
198)
199
200/* the default outvars name must not overwrite a control table that uses the
201 mddl_derivedvars default - this is the pair of defaults a caller is likely
202 to use */
203%mddl_derivedvars(libds=work.derived_vars)
204
205proc sql;
206insert into work.derived_vars
207 set libref='WORK',dsn='CLASS',var_nm='band',var_deps='AGE'
208 ,var_type='C',var_len=8,var_fmt='',active=1
209 ,var_logic="ifc(AGE>13,'TEEN','JUNIOR')";
210quit;
211
212%mp_derivedvars(
213 libds=work.class,
214 control=work.derived_vars,
215 outref=derref5,
216 outview=work.vw_default
217)
218
219%mp_assert(
220 iftrue=(0 < %mf_existvar(work.vw_default,BAND)),
221 desc=The default outvars does not overwrite the control table,
222 outds=work.test_results
223)
224
225%mp_assert(
226 iftrue=(%mf_nobs(work.derived_vars)=1),
227 desc=The control table survives the default outvars name,
228 outds=work.test_results
229)
230
231/* a filter containing quotes must not break the macro - a datetime literal
232 is the usual case, and quoting it again inside an %if condition does */
233%mp_derivedvars(
234 libds=work.class,
235 control=work.derived_vars,
236 where=%str(var_nm ne "NOPE"),
237 outref=derref11,
238 outvars=work.dvnames11,
239 outview=work.vw_wherequote
240)
241
242%mp_assertdsobs(work.dvnames11,
243 desc=A filter containing quotes is applied,
244 test=EQUALS 1,
245 outds=work.test_results
246)
247
248%mp_assert(
249 iftrue=(0 < %mf_existvar(work.vw_wherequote,BAND)),
250 desc=The view is still built when the filter contains quotes,
251 outds=work.test_results
252)
253
254/* a control table with no rows for the target leaves everything empty */
255%mddl_derivedvars(libds=work.dvcfg_empty)
256
257%mp_derivedvars(
258 libds=work.class,
259 control=work.dvcfg_empty,
260 outref=derref3,
261 outview=work.vw_empty,
262 outvars=work.dvnames3
263)
264
265%mp_assertdsobs(work.dvnames3,
266 desc=No rows in the control table means no variables,
267 test=EQUALS 0,
268 outds=work.test_results
269)
270
271%mp_assert(
272 iftrue=(%mf_existds(libds=work.vw_empty)=0),
273 desc=No view is built when there is nothing to derive,
274 outds=work.test_results
275)
276
277/* a missing control table is not an error, it just means no variables */
278%mp_derivedvars(
279 libds=work.class,
280 control=work.doesnotexist,
281 outref=derref4,
282 outvars=work.dvnames4
283)
284
285%mp_assertdsobs(work.dvnames4,
286 desc=A missing control table yields no variables,
287 test=EQUALS 0,
288 outds=work.test_results
289)
290
291/* a diamond graph - two variables built on one, then a fourth on both, then a
292 fifth on that. Rows are loaded in reverse name order so nothing but the
293 dependency sort can produce the right sequence */
294%mddl_derivedvars(libds=work.dvcfg_diamond)
295
296proc sql;
297insert into work.dvcfg_diamond
298 set libref='WORK',dsn='CLASS',var_nm='v5',var_deps='SEX V4'
299 ,var_type='C',var_len=40,var_fmt='',var_logic="cats(SEX,'-',put(V4,best12.))"
300 ,active=1;
301insert into work.dvcfg_diamond
302 set libref='WORK',dsn='CLASS',var_nm='v4',var_deps='V2 V3'
303 ,var_type='N',var_len=8,var_fmt='',var_logic='V2 + V3',active=1;
304insert into work.dvcfg_diamond
305 set libref='WORK',dsn='CLASS',var_nm='v3',var_deps='V1'
306 ,var_type='N',var_len=8,var_fmt='',var_logic='V1 + 1',active=1;
307insert into work.dvcfg_diamond
308 set libref='WORK',dsn='CLASS',var_nm='v2',var_deps='V1'
309 ,var_type='N',var_len=8,var_fmt='',var_logic='V1 * 2',active=1;
310insert into work.dvcfg_diamond
311 set libref='WORK',dsn='CLASS',var_nm='v1',var_deps='AGE'
312 ,var_type='N',var_len=8,var_fmt='',var_logic='AGE + 0',active=1;
313quit;
314
315%mp_derivedvars(
316 libds=work.class,
317 control=work.dvcfg_diamond,
318 outref=derref8,
319 outview=work.vw_diamond
320)
321
322%mp_assertcols(work.vw_diamond,
323 desc=The diamond graph is fully materialised,
324 cols=V1 V2 V3 V4 V5,
325 test=ALL,
326 outds=work.test_results
327)
328
329data work.chkdiamond;
330 set work.vw_diamond;
331 if v1 ne age then output;
332 if v2 ne age*2 then output;
333 if v3 ne age+1 then output;
334 if v4 ne age*3+1 then output;
335 if v5 ne cats(sex,'-',put(age*3+1,best12.)) then output;
336run;
337
338%mp_assertdsobs(work.chkdiamond,
339 desc=Every variable in the diamond graph resolves to the right value,
340 test=EQUALS 0,
341 outds=work.test_results
342)
343
344/* the assignment order must be v1, then v2 and v3, then v4, then v5 */
345data work.chkorder;
346 infile derref8 truncover;
347 input line $4000.;
348 seq=_n_;
349 length nm $32;
350 if prxmatch('/^V\d+\s*=/',strip(line)) then nm=scan(strip(line),1,'=');
351 else nm='';
352run;
353
354data work.chkorder2;
355 set work.chkorder end=eof;
356 retain s1 s2 s3 s4 s5;
357 if nm='V1' then s1=seq;
358 if nm='V2' then s2=seq;
359 if nm='V3' then s3=seq;
360 if nm='V4' then s4=seq;
361 if nm='V5' then s5=seq;
362 if eof;
363 if missing(s1) or missing(s2) or missing(s3) or missing(s4) or missing(s5)
364 then output;
365 else if s1 ge min(s2,s3) then output;
366 else if max(s2,s3) ge s4 then output;
367 else if s4 ge s5 then output;
368run;
369
370%mp_assertdsobs(work.chkorder2,
371 desc=The diamond graph is assigned in dependency order,
372 test=EQUALS 0,
373 outds=work.test_results
374)
375
376/* a label longer than the 256 limit, containing quotes, must still produce a
377 literal SAS can parse - the truncation cannot split a doubled quote */
378%mddl_derivedvars(libds=work.dvcfg_long)
379
380data work.longcfg;
381 length logic $4000;
382 logic=cats("ifc(AGE>13,'",repeat('a',300),"','short')");
383run;
384
385proc sql;
386insert into work.dvcfg_long
387 (libref,dsn,var_nm,var_deps,var_type,var_len,var_fmt,var_logic,active)
388 select 'WORK','CLASS','LONG_LABEL','AGE','C',400,'',logic,1
389 from work.longcfg;
390quit;
391
392%mp_derivedvars(
393 libds=work.class,
394 control=work.dvcfg_long,
395 outref=derref9,
396 outview=work.vw_long
397)
398
399data work.chkquotes;
400 infile derref9 truncover;
401 input line $4000.;
402 if index(line,"label='")>0 and mod(count(line,"'"),2) ne 0 then output;
403run;
404
405%mp_assertdsobs(work.chkquotes,
406 desc=A truncated label keeps an even quote count,
407 test=EQUALS 0,
408 outds=work.test_results
409)
410
411%mp_assert(
412 iftrue=(%mf_existds(libds=work.vw_long)=1),
413 desc=A label longer than the limit still builds the view,
414 outds=work.test_results
415)
416
417/* one control table can serve several tables - the LIBREF/DSN pair scopes the
418 definitions, so the same name means different things on each */
419data work.class2;
420 set sashelp.class;
421run;
422
423%mddl_derivedvars(libds=work.dvcfg_multi)
424
425proc sql;
426insert into work.dvcfg_multi
427 set libref='WORK',dsn='CLASS',var_nm='scaled',var_deps='AGE'
428 ,var_type='N',var_len=8,var_fmt='',var_logic='AGE + 100',active=1;
429insert into work.dvcfg_multi
430 set libref='WORK',dsn='CLASS2',var_nm='scaled',var_deps='AGE'
431 ,var_type='N',var_len=8,var_fmt='dollar12.2',var_logic='AGE + 200',active=1;
432quit;
433
434%mp_derivedvars(
435 libds=work.class,
436 control=work.dvcfg_multi,
437 outref=derref10,
438 outview=work.vw_multi1
439)
440
441%mp_derivedvars(
442 libds=work.class2,
443 control=work.dvcfg_multi,
444 outref=derref10,
445 outview=work.vw_multi2
446)
447
448data work.chkmulti;
449 merge work.vw_multi1(keep=name age scaled rename=(scaled=scaled1))
450 work.vw_multi2(keep=name age scaled rename=(scaled=scaled2));
451 by name;
452 if scaled1 ne age + 100 then output;
453 if scaled2 ne age + 200 then output;
454run;
455
456%mp_assertdsobs(work.chkmulti,
457 desc=The same variable name means different things per table,
458 test=EQUALS 0,
459 outds=work.test_results
460)
461
462/* a configured format is applied as given - asserted on the statement the
463 macro emits, which is what the view is built from */
464data work.chkfmt;
465 infile derref10 truncover;
466 input line $4000.;
467 if index(line,'format=DOLLAR12.2')>0 then output;
468run;
469
470%mp_assertdsobs(work.chkfmt,
471 desc=A configured format is applied,
472 test=EQUALS 1,
473 outds=work.test_results
474)
475
476%mp_assertscope(COMPARE,ignorelist=_SASJS_WEBOUT_HEADERS,outds=work.test_results)