Trucos SAS: Añadir ceros por la izquierda

Estos son unos pequeños trucos para conseguir añadir tantos ceros por la izquierda como necesitemos para conseguir un tamaño dado para una cadena. Vamos a imaginar, para los ejemplos que vamos a poner, que necesitamos conseguir que nuestro valor tenga siempre 10 caracteres y que en caso de necesitarlo le añadiremos tantos ceros por la izquierda como sea necesario.
Lo primero, no es lo mismo si partimos de un número o de una cadena de texto. Disponemos de un método para cada uno de esos casos. En todo caso, la salida del proceso tiene que ser siempre una cadena de texto.

Añadir ceros por la izquierda a un número

Creamos una tabla NUMEROS con unos cuantos números de ejemplo de distintas longitudes. Deseamos crear una cadena de texto que tenga 10 posiciones y que, en caso de necesitarlo, se complete con tantos ceros por la izquierda como haga falta.

data NUMEROS;
    format num best12.;
    informat num best12.;
    input num;
    datalines;
    646543589
    9684634311
    654634
    6354
    1
run;

data NUM_FORMAT;
    set NUMEROS;
    format cadena $10.;
    cadena = put(num,z10.);
run;

Lo que hemos hecho para resolverlo es un casting del campo original. Con put() le hemos aplicado un formato z10., que es el que se encarga de añadirle ceros por la izquierda. La salida de esto es una cadena de texto, ya que si almacenamos un número, los ceros por la izquierda se perderán.

Añadir ceros por la izquierda a una cadena de texto

En caso de que partamos de una cadena de texto hay varias soluciones posibles, si bien, alguna de ellos no es válida si tenemos caracteres alfanuméricos en la cadena origen. Incluyo algunas en el siguiente código incluyéndolas como distintos campo de la tabla de salida:

data CADENAS;
    format char $10.;
    informat char $10.;
    input char;
    datalines;
    615546241
    A165432113
    135485
    A
run;

data CHAR_FORMAT;
    set CADENAS;
    format cadena1 cadena2 $10.;
    cadena1 = compress(repeat('0',9 - length(char))||char);
    cadena2 = put(input(char,best12.),z10.);
run;

La primera solución utiliza la función repeat() que incluye tantos caracteres, ‘0’ en este caso, como se le indique. Aquí se da una particularidad que no es común en SAS: si se indica 2 se añadirán 3 ceros, por lo que hay que restar 1 a la cantidad de caracteres que queremos añadir. Por ello, el segundo parámetro de repeat() está calculado como 9 (en vez de 10) menos la longitud original de la cadena. Concatenando la salida del repeat() a la cadena original tendremos lo que estamos buscando.
Otra opción es una solución menos general porque solo es válida si la cadena de entrada está formada solo por números. Esto es porque lo que haremos será reformatear la cadena de texto primero como número (formato best12.) para luego convertirla de nuevo en texto con el formato z10. que es el que vimos que se encarga de añadir ceros por la izquierda a números. Tenemos que usar una combinación de put() e input() ya que cada uno de ellos acepta números o cadenas de texto respectivamente.

Salida del código anterior

Macro SAS: Algoritmo para validar el código de cuenta bancaria (IBAN)

Aquí tenemos una macro que sirve para validar el código de cuenta bancario, IBAN, que rige en muchos países de Europa, Caribe y Medio Oriente. Se le pasan como parámetros el nombre de la tabla y el nombre del campo que contiene el IBAN en formato electrónico (todos seguido, sin espacios) y devuelve un indicador de si es válido o no y el nomobre de la entidad bancaria a la que pertenece.

El código IBAN tiene tres partes, los dos primeros dígitos corresponden al país, los dos segundos son el código de verificación que se calculan en función de todos los demás y que validan que el código se ha escrito bien; y una última parte que es variable. Cada país define la estructura de esta tercera parte que es un código alfanumérico de hasta 30 caracteres, ya que el código IBAN tiene un máximo de 34 caracteres totales.

En el caso de España, esta tercera parte del IBAN corresponde a 4 dígitos para el código de la entidad bancaria tal y como se lo asigna el Banco de España, otros 4 dígitos para el código de la oficina bancaria, los dos siguientes corresponden al código de verificación del antiguo CCC (el antiguo número de cuenta que teníamos en España), y finalmente 10 caracteres para el número de cuenta bancaria. El total de caracteres para España son 24.

Esta macro valida el código del país, que tiene que estar incluido en la lista de países adjunta a este post, el código de oficina bancaria para los IBAN españoles (también según listado adjunto). Será necesario crear las tabla SAS PAISES y BANCOS respectivamente para que la macro funcione plenamente. Lamentablemente, solo valida códigos IBAN españoles.

%macro normalizar_ccc(tabla=,campo=);
    /* Identificar país */
    data norm1;
        set &tabla;
        &campo = upcase(&campo);
        pais = substr(&campo,1,2);
        verificador = substr(&campo,3,2);
        resto = substr(&campo,5);
    run;

    /* Verificando pais y longitud */
    proc sql;
        create table norm2 as
        select a.*,
               longitud,
               case when length(&campo) ne longitud then 0
                    when b.codigo_pais = '' then 0
                    else 1 end as ind_valido
        from norm1 a
        left join IBAN b
        on a.pais = b.codigo_pais and a.pais = 'ES';
    quit;

    /* Verificar banco */
    proc sql;
        create table norm3 as
        select a.*,
               b.banco
        from norm2 a
        left join BANCOS b
        on substr(&campo,5,4) = input(put(b.codigo_banco,z4.),$4.);
    quit;

    data norm4;
        set norm3;
        if banco = '' then ind_valido = 0;
    run;

    /* validar codigo control */
    data norm5 (drop=calculo: modulo);
        set norm4;
        format calculo1 calculo2 calculo3 32.;
        if ind_valido = 1 and longitud = 24 then do;
            calculo1 = 0;
            calculo2 = 0;
            %do x = 5 %to 17;
                calculo1 = calculo1 * 10;
                if anyalpha(substr(&campo,&x,1)) = 1 then do;
                    calculo1 = calculo1 * 10;
                    calculo1 = calculo1 + rank(substr(&campo,&x,1))-55;
                end;
                else do;
                    calculo1 = calculo1 + put(substr(&campo,&x,1),8.);
                end;
            %end;
            modulo = mod(calculo1,97);
            %do x = 18 %to 24;
                calculo2 = calculo2 * 10;
                if anyalpha(substr(&campo,&x,1)) = 1 then do;
                    calculo2 = calculo2 * 10;
                    calculo2 = calculo2 + rank(substr(&campo,&x,1))-55;
                end;
                else do;
                    calculo2 = calculo2 + put(substr(&campo,&x,1),8.);
                end;
            %end;
            %do x = 1 %to 4;
                calculo2 = calculo2 * 10;
                if anyalpha(substr(&campo,&x,1)) = 1 then do;
                    calculo2 = calculo2 * 10;
                    calculo2 = calculo2 + rank(substr(&campo,&x,1))-55;
                end;
                else do;
                    calculo2 = calculo2 + put(substr(&campo,&x,1),8.);
                end;
            %end;
            calculo3 = (modulo * (10**ceil(log10(round(calculo2))))) + calculo2;
            modulo = mod(calculo3,97);
            if modulo ne 1 then do;
                ind_valido = 0;
                banco = "";
            end;
        end;
    run;

    /* salida */
    proc sql;
        create table &tabla._ as
        select a.*, ind_valido, banco
        from &tabla a
        left join norm5 b
        on a.&campo = b.&campo;
    quit;
%mend normalizar_ccc;

Descárgate este Excel que contiene los datos que debes incluir en la tabla PAISES y BANCOS:

El algoritmo para validar el IBAN partiendo del número de cuenta, consiste en lo siguiente:
1) Colocer los cuatro primeros caracteres detrás de todos los demás.
2) Sustituir todos los caracteres alfanuméricos que pueda haber por un equivalente numérico de 2 cifras de forma que A=10, B=11, C=12, etc.
3) Dividir la cifre restante entre 97 y tomar el resto de esa división.
4) Si el resto es un 1, entonces el IBAN es correcto.

Para validar la macro con algunos ejemplos de prueba, puedes utilizar lo siguiente:

data CUENTAS;
    format ccc $50.;
    length ccc $ 50;
    input ccc $;
    datalines;
ES6621000418401234567891
ES6000491500051234567892
ES9420805801101234567891
ES90002469125012345678910
ES7100302053091234567895
EL71003020530943574567895
ES1000492352082414205416
ES1720852066623456789011
ES4501986746463463463547
ES450198674646D463463547
ES450198674646&463463547
ES450198674646.463463547
ES1346657676431313200000
run;

%normalizar_ccc(tabla=CUENTAS,campo=ccc);

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.

SAS: Operar con fechas: función intnx()

La función intnx() es una de las funciones más útiles, junto con intck(), para el trabajo con fechas y merece por si misma una entrada en el blog. Vamos a empezar viendo su sintaxis básica y su uso:

Sintáxis

intnx(intervalo, fecha, incremento)
Esta función calcula de una forma fácil la fecha que se deriva de sumar cierta cantidad (incremento) de periodos de tiempo (intervalo) a una fecha concreta. Por ejemplo, intnx('month','1jan2020'd,5) da como resultado el 1 de junio de 2020, porque si a partir del 1 de enero sumamos 5 meses es el 1 de junio. Esto es tremendamente útil porque sumar meses considerándolos como 30 días no da una fecha concreta del mes que buscamos si no que varía en función de la cantidad de días que tengan los meses intermedios.

Con respecto a los intervalos que podemos usar están, entre otros: ‘day’, ‘week’, ‘month’, ‘qtr’ (trimestre), ‘year’. La fecha debe incluirse en formato numérico (no vale una cadena de texto) y el incremento es cualquier número entero positivo o negativo.

intnx(‘month’,’1jul2021’d,-7) ‘1jan2021’d
intnx(‘day’,’15jul2021’d,-15) ‘1jul2021’d
intnx(‘week’,’11oct2021’d,-1) ‘4oct2021’d
intnx(‘year’,’1jan2021’d,-1) ‘1jan2020’d
intnx(‘qtr’,’1jan2021’d,-1) ‘1oct2020’d

El alineamiento

Adicionalmente disponemos de un cuarto parámetro; el alineamiento, que indica en qué parte del intervalo temporal que hemos definido queremos situarnos: al principio, a la mitad, al final o el mismo día que la fecha indicada. El alineamiento tomará respectivamente los valores: ‘B’, ‘M’, ‘E’, ‘S’, por sus siglas en inglés. El valor ‘B’ es el valor por defecto si no se indica nada.

intnx(‘year’,’6jul2021’d,0) ‘1jan2021’d Como ‘B’ es el valor por defecto, si no indicamos nada obtendremos el primer día del mismo (0) periodo (year) de la fecha indicada (‘6jul2021’).
intnx(‘year’,’6jul2021’d,0,’E’) ’31dec2021’d Cuando se indica ‘E’ obtendremos el último día del mismo (0) periodo (year) a la fecha indicada (‘6jul2021’).
intnx(‘year’,’6jul2021’d,1,’S’) ‘6jul2022’d Con ‘S’ obtendremos la misma fecha del siguiente periodo.
intnx(‘week’,’6jul2021’d,1,’S’) ’13jul2021’d En este caso, como se indican sumar un periodo de una semana y tenemos el alineamiento nos indica que debe ser el mismo día de la semana (martes), obtendremos la fecha de 7 días después.
intnx(‘week’,’6jul2021’d,1,’E’) ’17jul2021’d Utilizar el alineamiento ‘E’ indica tener que ir al último día de la siguiente semana, el sábado. Las semanas comienzan el domingo.

Con todo esto, ¿cómo obtener los registros correspondientes al mes anterior de una tabla cuyos registros están informados con un campo fecha?
intnx('month',date(),-1,'B') <= fecha <= intnx('month',date(),-1,'E')
Esta condición permite obtener todos los registros del mes anterior al actual de una tabla. Lo mismo sería para obtener la semana anterior o el trimestre anterior.

Multiplicadores de intervalo

Finalmente, existe la posibilidad de añadir multiplicadores a los intervalos. Para ello, se añade simplemente un número detrás del nombre del intervalo. Por ejemplo: intnx('month3',date(),1) sería una forma de indicar la fecha correspondiente al inicio del próximo trimestre y es equivalente a expresarlo como intnx('qtr',date(),1).

intnx(‘month3’,’1jan2021’d,1) ‘1apr2021’d Obtenemos el primer día del trimestre siguiente al actual.
intnx(‘month2’,’1jan2021’d,1) ‘1mar2021’d Utilizando el multiplicador 2 con el mes obtendremos periodos de 2 meses.
intnx(‘month2.2′,’1mar2021’d,0,’B’) ‘1feb2021’d Aquí utilizamos el multiplicador para definir periodos temporales de dos meses, pero lo hemos modificado con un índice «.2» que indica que estos periodos temporales de dos meses comienzan el segundo mes del año. Por tento la fecha de inicio de ese periodo es el 1 de febrero.
intnx(‘month2.2′,’1mar2021’d,0,’E’) ’31mar2021’d Al igual que en el ejemplo anterior, utilizamos el mismo multiplicador y pedimos la última fecha de ese periodo que es el 31 de marzo. ‘month2.1’ sería equivalente a ‘month2’.

Intervalos personalizados

Vistas todas las opciones que nos ofrece predefinidas intnx() aún podemos definir intervalos propios en vez de los conocidos ‘month’ o ‘year’. Pongámonos en el caso de una empresa que realice los cierres mensuales el día 16 de cada mes. Una opción sería utilizar el intervalo predefinido ‘semimonth2.2’.

intnx(‘semimonth2.2′,’1jan2021’d,0,’B’) ’16dec2020’d
intnx(‘semimonth2.2′,’1jan2021’d,0,’E’) ’15jan2021’d

Sin embargo, esta empresa tiene una particularidad, y es que los meses de febrero cierran el día 14, no el 15. Para resolver esto tenemos dos opciones o añadir una lógica condicionada al mes en curso o definir nuestro propio intervalo. Para definir nuestro propio intervalo utilizaremos la instrucción options intervalds a la que se le pasan el nombre del intervalo y el de la tabla que contiene la definición de esos intervalos. Luego definimos esa tabla de la manera en que se muestra y ya podremos utilizar nuestros propios intervalos con la función intnx() y de la misma manera que los predefinidos:

options intervalds=(cierre=dstest);
data dstest;
format begin end date9.;
begin='16jan2021'd; end='14feb2021'd; output;
begin='15feb2021'd; end='15mar2021'd; output;
begin='16mar2021'd; end='15apr2021'd; output;
begin='16apr2021'd; end='15jun2021'd; output;
begin='16jun2021'd; end='15jul2021'd; output;
begin='16jul2021'd; end='15aug2021'd; output;
begin='16aug2021'd; end='15sep2021'd; output;
begin='16sep2021'd; end='15oct2021'd; output;
begin='16oct2021'd; end='15nov2021'd; output;
begin='16nov2021'd; end='15dec2021'd; output;
begin='16dec2021'd; end='15jan2022'd; output;
begin='16jan2022'd; end='14feb2022'd; output;
begin='15feb2022'd; end='15mar2022'd; output;
begin='16mar2022'd; end='15apr2022'd; output;
begin='16apr2022'd; end='15jun2022'd; output;
begin='16jun2022'd; end='15jul2022'd; output;
begin='16jul2022'd; end='15aug2022'd; output;
begin='16aug2022'd; end='15sep2022'd; output;
begin='16sep2022'd; end='15oct2022'd; output;
begin='16oct2022'd; end='15nov2022'd; output;
begin='16nov2022'd; end='15dec2022'd; output;
begin='16dec2022'd; end='15jan2023'd; output;
run;

data salida;
format fecha_inicio fecha_fin date9.;
fecha_inicio=intnx('cierre',date(),0,'B');
fecha_fin=intnx('cierre',date(),0,'E');
run;