mirror of
https://github.com/sasjs/core.git
synced 2026-07-23 15:35:29 +00:00
91 lines
3.1 KiB
SAS
91 lines
3.1 KiB
SAS
/**
|
|
@file
|
|
@brief Iterative row-level MD5 hash generator
|
|
@details Generates DATA step statements that compute a deterministic,
|
|
row-level MD5 hash using a Merkle-style construction. Each variable is
|
|
hashed independently and the resulting raw 16-byte digests are combined
|
|
iteratively. This avoids concatenating the per-variable hex digests into a
|
|
single SAS character expression, which overflows when datasets contain many
|
|
variables.
|
|
|
|
Temporary variables use five leading underscores.
|
|
|
|
This macro is called from inside a DATA step. The caller is responsible
|
|
for declaring the output hash column if it does not already exist on the
|
|
input dataset.
|
|
|
|
Variables are hashed in the order supplied. If a particular variable
|
|
needs to influence the hash first (for example the previous row hash in
|
|
`mp_hashdataset`, or business dates in Data Controller's bitemporal
|
|
loader) simply list it first in `cvars` or `nvars`.
|
|
|
|
@param [in] md5_col= Name of the output hash column.
|
|
@param [in] cvars= Space separated list of character variables to hash.
|
|
@param [in] nvars= Space separated list of numeric variables to hash.
|
|
|
|
@version 9.3M5
|
|
@author Allan Bowe
|
|
**/
|
|
|
|
%macro mp_rowhash(
|
|
md5_col=
|
|
,cvars=
|
|
,nvars=
|
|
);
|
|
|
|
/* DATA step temp variables use five leading underscores. The names
|
|
are generated at macro invocation to avoid clashing with data columns. */
|
|
%local state digest pair numtext normal i chars nums;
|
|
%let state=%mf_getuniquename(prefix=_____state_);
|
|
%let digest=%mf_getuniquename(prefix=_____digest_);
|
|
%let pair=%mf_getuniquename(prefix=_____pair_);
|
|
%let numtext=%mf_getuniquename(prefix=_____numtext_);
|
|
%let normal=%mf_getuniquename(prefix=_____normal_);
|
|
%let i=%mf_getuniquename(prefix=_____i_);
|
|
%let chars=%mf_getuniquename(prefix=_____chars_);
|
|
%let nums=%mf_getuniquename(prefix=_____nums_);
|
|
|
|
length &state $16
|
|
&digest $16
|
|
&pair $32
|
|
&numtext $64
|
|
&normal &i 8;
|
|
if _n_=1 then call missing(&state,&digest,&pair,&numtext,&normal,&i);
|
|
drop &state &digest &pair &numtext &normal &i;
|
|
|
|
/* Versioned seed prevents confusion with other hashing schemes. */
|
|
&state = md5('DC HASH v2');
|
|
|
|
%if %length(&cvars)>0 %then %do;
|
|
array &chars{*} $ &cvars;
|
|
do &i = 1 to dim(&chars);
|
|
/* Leading blanks are retained. */
|
|
&digest = md5(trimn(&chars[&i]));
|
|
substr(&pair, 1, 16) = &state;
|
|
substr(&pair, 17, 16) = &digest;
|
|
&state = md5(&pair);
|
|
end;
|
|
%end;
|
|
|
|
%if %length(&nvars)>0 %then %do;
|
|
array &nums{*} &nvars;
|
|
do &i = 1 to dim(&nums);
|
|
/*
|
|
multiply-by-one for consistent cross-system precision.
|
|
Ignore null to protect SAS special missing values.
|
|
Cannot use IFN() as it evaluates both sides
|
|
*/
|
|
if missing(&nums[&i]) then &normal = &nums[&i];
|
|
else &normal = &nums[&i] * 1;
|
|
&numtext = put(&normal, binary64.);
|
|
&digest = md5(trim(&numtext));
|
|
substr(&pair, 1, 16) = &state;
|
|
substr(&pair, 17, 16) = &digest;
|
|
&state = md5(&pair);
|
|
end;
|
|
%end;
|
|
|
|
&md5_col = put(&state, $hex32.);
|
|
|
|
%mend mp_rowhash;
|