Macro SAS: Validar DNI de Perú

Hoy voy a incluir una macro que sirve para validar con SAS un número de DNI peruano verificando si el dígito verificador es correcto o no.
Hay que empezar diciendo que inicialmente, y hasta 2007, los DNIs sin fecha de caducidad tenían un dígito verificador que correspondía con una de estas letras: A, B, C, D, E, F, G, H, I, J o K. En el resto de casos se asigna un número de 0 a 9 que sirve para verificar el resto de dígitos del DNI a través de una serie de operaciones matemáticas.
Para trabajar con algunos ejemplos alimentaremos la macro con la siguiente tabla con DNIs a evaluar:

/* Ejemplos*/;
data DNIS;
    format DNI $10.;
    input DNI;
    datalines;
    67415321-0
    1657351-A
    31874-1
    671354134
run;

La macro %validarDNI añade dos campos a la tabla que se le pase por parámetro: dni_normal y ind_valido. dni_normal es el valor del campo que contenía el DNI originalmente, pero normalizado. ind_valido toma dos valores posibles: 1 que indica que el DNI es correcto y 0 que indica que es erróneo.
%validarDNI acepta además dos parámetros obligatorios: el nombre de la tabla y el nombre del campo DNI a validar dentro de ella.

/*Macro*/;
%macro validarDNI(tabla=, campo=);
    data DNI1 (drop=a valor resto codigo:);
        set &tabla;
        rename &campo=dni_original;
        dni_normal = upcase(compress(&campo,'-_. '));
        a = 9 - length(dni_normal);
        if a > 0 then dni_normal = compress(repeat('0',a-1) || dni_normal);
        ind_valido = 1;
        if length(dni_normal) > 9 then ind_valido = 0;
        %do i = 1 %to 8;
            if 0 > put(substr(dni_normal,&i,1),8.) or put(substr(dni_normal,&i,1),8.) > 9 then ind_valido = 0;
        %end;
        if substr(dni_normal,9,1) not in ('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K') then ind_valido = 0;
        if ind_valido > 0 then do;
            valor = 3*put(substr(dni_normal,1,1),8.) +
                    2*put(substr(dni_normal,2,1),8.) +
                    7*put(substr(dni_normal,3,1),8.) +
                    6*put(substr(dni_normal,4,1),8.) +
                    5*put(substr(dni_normal,5,1),8.) +
                    4*put(substr(dni_normal,6,1),8.) +
                    3*put(substr(dni_normal,7,1),8.) +
                    2*put(substr(dni_normal,8,1),8.);
            resto = mod(valor,11);
            if resto = 0 then resto = 11;
            resto = resto + 1;
            codigo1 = substr('67890112345',resto,1);
            codigo2 = substr('KABCDEFGHIJ',resto,1);
            if substr(dni_normal,9) ne codigo1 and substr(dni_normal,9) ne codigo2 then ind_valido = 0;
        end;
    run;

    data &tabla;
        set DNI1;
        rename dni_original = &campo;
    run;
%mend;
%validarDNI(tabla=DNIS, campo=DNI);

El algoritmo para calcular el dígito verificador del DNI es el siguiente: se multiplica cada número del DNI normalizado (sus 8 primeros dígitos) por el dígito que ocupe la misma posición en la cadena: 1, 7, 8, 0, 1, 1, 4, 6 y luego se suman todos los factores para dar una cifra de la que calcularemos el resto con resto a dividirla entre 11 (aquí, si el resto es 0 tomaremos 11).
Restamos 11 menos la resultante de la operación anterior. Le sumaremos 1 y entonces tomaremos ese valor para buscar el dígito correspondiente a esa posición en la cadena: 6, 7, 8, 9, 0, 1, 1, 2, 3, 4, 5. Este último es el dígito de verificación.

Macro SAS: Validar y normalizar el DNI

Comparto con vosotros esta macro que realiza una validación del campo DNI de una tabla. Admite como parámetros el nombre de la tabla (DNIS) y el nombre del campo con el DNI (DNI). Como salida genera otra tabla que se llama DNIS_ (añade un subrallado al final) y le añade el campo dni_norm. La macro normaliza DNIs y NIEs españoles.

En el primer paso se verifica el formato del DNI y se rechazan los que tengan un formato que no sea compatible. Estos se marcan con el indicador ind_valido. Para aquellos que cumplen con el formato básico de los DNIs, trocea ese DNI en prefix, number y sufix, De forma que se validan por separado.

En el segundo paso se normaliza la parte numérica del DNI dándole tantos dígitos como vaya a necesitar añadiendo ceros por la izquierda. En el tercer paso se calcula la letra del DNI/NIE y se juntan todos los trozos para tener el DNI normalizado. En un último paso se realiza el cruce que la tabla inicial de DNIs.

%macro normalizar_dni(tabla=,campo=);
    /* obtenemos las partes del DNI y localizamos formatos incorrectos */
    data norm1;
        set &tabla;
        &campo = upcase(&campo);
        if anypunct(&campo) > 0 then ind_valido = 0;
        else if length(&campo) > 9 then ind_valido = 0;
        else if 0 < anyalpha(substr(&campo,2)) < length(substr(&campo,2)) then ind_valido = 0;
        else if compress(substr(&campo,1,1),'XYZ','D') ne '' then ind_valido = 0;
        else if length(&campo)=9 and anyalpha(&campo)=0 and substr(&campo,1,1)='0' then do;
            &campo=substr(&campo,2);
            ind_valido = 1;
            prefix = compress(substr(&campo,1,1),'','D');
            number = input(compress(&campo,'','A'),8.);
            sufix = compress(substr(&campo,length(&campo)),'','D');
        end;
        else if length(&campo)=9 and anyalpha(&campo)=0 then ind_valido = 0;
        else do;
            ind_valido = 1;
            prefix = compress(substr(&campo,1,1),'','D');
            number = input(compress(&campo,'','A'),8.);
            sufix = compress(substr(&campo,length(&campo)),'','D');
        end;
    run;

    /* normalizamos el número */
    data norm2;
        set norm1;
        format numero $8.;
        length numero $ 8;
        n = number;
        if anyalpha(prefix)=1 then numero = put(number,z7.);
            else numero = put(number,z8.);
    run;

    /* calculamos la letra del DNI y verificamos si es correcta cuando venga informada */
    data norm3 (drop=prefix number sufix numero n letras resto letra_norm ind_valido);
        set norm2;
        letras = 'TRWAGMYFPDXBNJZSQVHLCKE';
        if prefix='Y' then n=n+10000000;
        if prefix='Z' then n=n+20000000;
        resto = mod(n,23);
        letra_norm = substr(letras,resto+1,1);
        dni_norm = compress(prefix||numero||letra_norm);
    run;

    /* salida */
    proc sql;
        create table &tabla._ as
        select a.*, dni_norm
        from &tabla a
        left join norm3 b
        on a.&campo = b.&campo;
    quit;
%mend normalizar_dni;

Podéis probarla con este set de datos de prueba que os dejo aquí:

data DNIS;
    format dni $20.;
    length dni $ 20;
    input dni $;
    datalines;
    16634732A
    1b
    16634732
    32631459w
    X00123
    Y1234612
    123&134
    Z1224536
    X00z12
    064563314
    6843131058
    1635740.65
    000000315
    G12
run;

%normalizar_dni(tabla=DNIS,campo=dni);