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
 

No hay comentarios.: