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