lunes, 18 de abril de 2016

Códigos de barra y QR como servicio

En varios de mis desarrollos, en distintos leguajes, tuve la necesidad de generar algún tipo de codigo de barra y últimamente también necesité generar códigos QR. El problema siempre fue como conseguir integrar esa funcionalidad al desarrollo, ya sea como biblioteca nativa o consumiendo algún servicio externo.

En ambos casos he encontrado que las desventajas son varias y el riesgo usarlas era importante. En el caso de las bibliotecas nativas es que no siempre se encuentra una para el lenguaje que estamos usando, ya sea por que no exista, por la licencia, costo, etc.

En el caso de los servicios externos , aunque es muy cómodo simplemente llamar al servicio y recuperar una imágen, estos pueden dejar de existir, volverse muy lentos o simplemente el acceso a internet no es una opción.

Por este motivo me propuse crear un servidor sencillo que me permita exportar como servicios la generación de ciertos códigos de barra y QR (los que he necesitado) de forma local.

La dirección del proyecto es esta:  https://github.com/padiazg/barcode-as-a-service

Actualmente puede generar codigos EAN13, CODE39, CODE129, PDF417 y QR. La lista puede extenderse a los demás tipos de código que soporta el paquete rescode en el que se basa este proyecto.

Para ponerlo a funcionar debemos seguir estos pasos:
  1. Tener instalado Node.js (se recomienda la versión mas actual)
  2. Descargar el proyecto en alguna carpeta
  3. Abrir una consola e ir a la carpeta donde acabamos de descargar el proyecto
  4. Ejecutar npm install para descargar los paquetes requeridos
  5. Ejecutar node barcode-service
Una vez corriendo el servicio, podemos apuntar un navegador al puerto 3000 de la máquina para tener un breve ejemplo del uso del servicio.

Se agradecerá los comentarios y algún feedback.

Actualización: 

V1.1 (09/mayo/2016)
  • Se corrige un problema con las letras, relacionado a la escala del código generado.
  • Se agrega el parámetro "scale", con la que se puede especificar la escala de la imágen generada.
  • Se agrega el parámetro "fmt" para seleccionar el formato de la imágen generada.
  • Se implementa la generación de códigos "DataMatrix" 
  • Se  simplifica el procedimiento interno para la generación de los códigos.
Para la generación de salida en formato JPG se debe tener instalado GraphicsMagick, es simplemente bajar e instalar la versión apropiada para su sistema, antes de ejecutar el servidor.

No debemos olvidar de actualizar los requerimientos de paquetes con npm install después de actualizar a la nueva versión.

miércoles, 13 de abril de 2016

Implementación algoritmo Soundex Español en Genexus

El algoritmo soundex es un algoritmo fonético, diseñado para indexar nombres por su pronunciación en Inglés. Es por esto que no es preciso cuando lo aplicamos a nombres o palabras en español, podría decirse incluso que no es usable en nuestro idioma.

Investigando un poco hace algunos años había encontrado esta entrada https://wiki.postgresql.org/wiki/SoundexESP, en la que me basé para crear sus equivalentes en distintos lenguages (VBA, T-SQL para Oracle). Por supuesto cuando se dió la necesidad le tocó a Genexus.

Implementación

La implementación la hice en un procedimiento llamado soundex, pero ustedes pueden llamarlo como mas les convenga

soundex:
/*
 Implementación del algoritmo Soundex para el idioma español
 
 variables  tipo
 ----------------------- -----------
 caracter  C(1)
 caracteres_buscar C(20)
 caracteres_reemplazar C(20)
 i   N(2)
 primera_letra  C(1)
 reemplazo  C(1)
 resto   C(20)
 soundex   C(20)
 texto   C(32)
 tmp   C(32)

        parámetros
 -----------------------
 in:&texto
 out:&soundex

*/

&soundex.SetEmpty()
&tmp = &texto.Trim().ToUpper()

// Si no hay texto a procesar retornamos un texto vacío
if &tmp.IsEmpty()
    return
endif

// realizamos un preprocesado del texto
do 'limpieza'

// aplicamos el algoritmo
do 'soundex'

Sub 'limpieza'
    /*     1) limpieza     */
    // eliminamos la H inicial (incluso si hay mas de una)
    &tmp = &tmp.ReplaceRegEx('^(H+)(.*)', '$2')

    // retornar vacío si no nos queda texto para analizar
    if &tmp.IsEmpty()
        return
    endif

    // eliminamos los acentos y la Ñ
    &caracteres_buscar     = 'ÑÁÉÍÓÚÀÈÌÒÙÜ'
    &caracteres_reemplazar = 'NAEIOUAEIOUU'
    for &i=1 to &caracteres_buscar.Length()
        &caracter = &caracteres_buscar.Substring(&i,1)
        if &caracteres_buscar.IndexOf(&caracter)>0
            &tmp = &tmp.Replace(&caracter, &caracteres_reemplazar.Substring(&i, 1))
        endif // &buscar.IndexOf(&caracter)>0
    endfor // &i=1 to &buscar.Length() ...

    // eliminamos caracteres no alfabéticos (números, signos, símbolos, etc)
    &tmp = &tmp.ReplaceRegEx('[^A-Z]', '')

    /*     2) ajustar primera letra    */  
    // fenómenos o casos especiales: GE y GI se convierten en JE y JI, CA en KA
    &primera_letra = &tmp.Substring(1,1)
    &resto = &tmp.Substring(2,&tmp.Length()-1) 

    do case
        case &primera_letra = 'V'
            &reemplazo = 'B'        // VACA -> BACA, VALOR -> BALOR

        case &primera_letra = 'Z' or &primera_letra = 'X'
            &reemplazo = 'S'        // ZAPATO -> SAPATO, XILÓFONO -> SILÓFONO

        case &primera_letra = 'G'
            and (&tmp.Substring(2,1)='E' or &tmp.Substring(2,1)='I')
            &reemplazo = 'J'        // GIMNASIO -> JIMNASIO, GERANIO -> JERANIO

        case &primera_letra = 'C'
            and &tmp.Substring(2,1)<>'H'
            and &tmp.Substring(2,1)<>'E'
            and &tmp.Substring(2,1)<>'I'
            &reemplazo = 'K'        // CASA -> KASA, COLOR -> KOLOR, CULPA -> KULPA

        otherwise
            &reemplazo = &primera_letra

    endcase

    &tmp = &reemplazo + &resto

    /*     3) corregir letras compuestas, volverlas una sola    */
    &tmp = &tmp.ReplaceRegEx('CH', 'V')
    &tmp = &tmp.ReplaceRegEx('QU', 'K')
    &tmp = &tmp.ReplaceRegEx('LL', 'J')
    &tmp = &tmp.ReplaceRegEx('CE', 'S')
    &tmp = &tmp.ReplaceRegEx('CI', 'S')
    &tmp = &tmp.ReplaceRegEx('YA', 'J')
    &tmp = &tmp.ReplaceRegEx('YE', 'J')
    &tmp = &tmp.ReplaceRegEx('YI', 'J')
    &tmp = &tmp.ReplaceRegEx('YO', 'J')
    &tmp = &tmp.ReplaceRegEx('YU', 'J')
    //&tmp = &tmp.ReplaceRegEx('GE', 'J')
    //&tmp = &tmp.ReplaceRegEx('GI', 'J')
    &tmp = &tmp.ReplaceRegEx('NY', 'N')
    &tmp = &tmp.ReplaceRegEx('NH', 'N') // anho, banho, tamanho, inhalador
EndSub // 'limpieza' ...

Sub 'soundex'

    /* 4) obtener primera letra        */
    &primera_letra = &tmp.Substring(1, 1)
  
    /* 5) obtener el resto del texto    *
    &resto = &tmp.Substring(2, &tmp.Length()-1)
   
    /* 6) en el resto, eliminar vocales y consonantes fonéticas        */
    &resto = &resto.ReplaceRegEx('[AEIOUHWY]', '')

    /* 7) convertir letras fonéticamente equivalentes a números. esto hace que B sea equivalente a V, C con S y Z, etc.    */
    &resto = &resto.ReplaceRegEx('[BPFV]',   '1')
    &resto = &resto.ReplaceRegEx('[CGKSXZ]', '2')
    &resto = &resto.ReplaceRegEx('[DT]',     '3')
    &resto = &resto.ReplaceRegEx('[L]',      '4')
    &resto = &resto.ReplaceRegEx('[MN]',     '5')
    &resto = &resto.ReplaceRegEx('[R]',      '6')
    &resto = &resto.ReplaceRegEx('[QJ]',     '7')

    // eliminamos números iguales adyacentes
    &resto = &resto.ReplaceRegEx('(\d)\1+', '$1') 
    &soundex = &primera_letra + &resto.Trim()
    if &soundex.Length() < 4
        &soundex = padr(&soundex, 4, '0')
    else
        &soundex = &soundex.Substring(1,4)
    endif
    
EndSub // 'soundex' ...

Prueba

A continuación podemos hacer unas pruebas para ver el algoritmo en funcionamiento, creamos otro procedimiento llamado prueba_soundex:

prueba_soundex:
/*
 Colocar los siguientes valores en las propiedades del procedimiento
 -------------------------------------------------------------------
 Main program: true
 Call protocol: Command Line
*/

msg('hola => ' + soundex.Udp('hola'), status)
msg('ola => ' + soundex.Udp('ola'), status)

msg('zapato => ' + soundex.Udp('zapato'), status)
msg('sapato => ' + soundex.Udp('sapato'), status)

msg('Jimenez => ' + soundex.Udp('Jimenez'), status)
msg('Jiménez => ' + soundex.Udp('Jiménez'), status)
msg('Jimenes => ' + soundex.Udp('Jimenes'), status)
msg('Jiménes => ' + soundex.Udp('Jiménes'), status)
msg('Gimenez => ' + soundex.Udp('Gimenez'), status)
msg('Giménez => ' + soundex.Udp('Giménez'), status)
msg('Gimenes => ' + soundex.Udp('Gimenes'), status)
msg('Giménes => ' + soundex.Udp('Giménes'), status)

msg('Díaz => ' + soundex.Udp('Díaz'), status)
msg('días => ' + soundex.Udp('días'), status)
msg('dias => ' + soundex.Udp('dias'), status)

msg('mejico => ' + soundex.Udp('mejico'), status)
msg('mexico => ' + soundex.Udp('mexico'), status)

Luego ejecutamos prueba_soundex y deberíamos de obtener la siguiente salida:
"C:\Program Files\Java\jdk1.8.0_25\bin\java.exe"  atest_soundex
hola => O400
ola => O400
zapato => S130
sapato => S130
Jimenez => J520
Jiménez => J520
Jimenes => J520
Jiménes => J520
Gimenez => J520
Giménez => J520
Gimenes => J520
Giménes => J520
Díaz => D200
días => D200
dias => D200
mejico => M720
mexico => M200
Execution Success
Como pueden ver el algoritmo es bastante preciso con palabras similares, aunque sigo trabajando en generalizar el caso de ji y xi.  

Pensamientos finales 

Los escenarios donde aplicar eso de forma práctica pueden variar mucho, de hecho no se si podría usarse efectivamente con nombres completos.

En mi caso lo estuve utilizando para crear un diccionario de una tabla de nombres de calles. Primero se procesan las calles registradas (normalizadas), se separan en sus palabras componentes y se registran individualmente en un diccionario con su correspondiente código soundex. Se guarda la calle y su relación con sus palabras componentes.

Al introducir un nombre de calle para buscar, esta también se separa en sus palabras componentes, se calcula el código soundex para cada una y son estos códigos los que se buscan en el diccionario, por supuesto que el proceso es mas complejo y tiene mas validaciones, pero básicamente es así como funciona.

El código de ambos procedimientos pueden encontrarlos en https://github.com/padiazg/soundex-genexus