Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mp_rowhash.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Iterative row-level MD5 hash generator
4 @details Generates DATA step statements that compute a deterministic,
5 row-level MD5 hash using a Merkle-style construction. Each variable is
6 hashed independently and the resulting raw 16-byte digests are combined
7 iteratively. This avoids concatenating the per-variable hex digests into a
8 single SAS character expression, which overflows when datasets contain many
9 variables.
10
11 Temporary variables use five leading underscores.
12
13 This macro is called from inside a DATA step. The caller is responsible
14 for declaring the output hash column if it does not already exist on the
15 input dataset.
16
17 Variables are hashed in the order supplied. If a particular variable
18 needs to influence the hash first (for example the previous row hash in
19 `mp_hashdataset`, or business dates in Data Controller's bitemporal
20 loader) simply list it first in `cvars` or `nvars`.
21
22 @param [in] md5_col= Name of the output hash column.
23 @param [in] cvars= Space separated list of character variables to hash.
24 @param [in] nvars= Space separated list of numeric variables to hash.
25
26 @version 9.3M5
27 @author Allan Bowe
28**/
29
30%macro mp_rowhash(
31 md5_col=
32 ,cvars=
33 ,nvars=
34 );
35
36 /* DATA step temp variables use five leading underscores. The names
37 are generated at macro invocation to avoid clashing with data columns. */
38 %local state digest pair numtext normal i chars nums;
39 %let state=%mf_getuniquename(prefix=_____state_);
40 %let digest=%mf_getuniquename(prefix=_____digest_);
41 %let pair=%mf_getuniquename(prefix=_____pair_);
42 %let numtext=%mf_getuniquename(prefix=_____numtext_);
43 %let normal=%mf_getuniquename(prefix=_____normal_);
44 %let i=%mf_getuniquename(prefix=_____i_);
45 %let chars=%mf_getuniquename(prefix=_____chars_);
46 %let nums=%mf_getuniquename(prefix=_____nums_);
47
48 length &state $16
49 &digest $16
50 &pair $32
51 &numtext $64
52 &normal &i 8;
53 if _n_=1 then call missing(&state,&digest,&pair,&numtext,&normal,&i);
54 drop &state &digest &pair &numtext &normal &i;
55
56 /* Versioned seed prevents confusion with other hashing schemes. */
57 &state = md5('DC HASH v2');
58
59 %if %length(&cvars)>0 %then %do;
60 array &chars{*} $ &cvars;
61 do &i = 1 to dim(&chars);
62 /* Leading blanks are retained. */
63 &digest = md5(trimn(&chars[&i]));
64 substr(&pair, 1, 16) = &state;
65 substr(&pair, 17, 16) = &digest;
66 &state = md5(&pair);
67 end;
68 %end;
69
70 %if %length(&nvars)>0 %then %do;
71 array &nums{*} &nvars;
72 do &i = 1 to dim(&nums);
73 /*
74 multiply-by-one for consistent cross-system precision.
75 Ignore null to protect SAS special missing values.
76 Cannot use IFN() as it evaluates both sides
77 */
78 if missing(&nums[&i]) then &normal = &nums[&i];
79 else &normal = &nums[&i] * 1;
80 &numtext = put(&normal, binary64.);
81 &digest = md5(trim(&numtext));
82 substr(&pair, 1, 16) = &state;
83 substr(&pair, 17, 16) = &digest;
84 &state = md5(&pair);
85 end;
86 %end;
87
88 &md5_col = put(&state, $hex32.);
89
90%mend mp_rowhash;