Soundex
February 16, 2010
This task is harder than it looks; I will never admit how many wrong tries I made before I got a working result. Here’s my version:
(define (soundex str)
(define (code c)
; ABCDEFGHIJKLMNOPQRSTUVWXYZ
(string-ref "01230120022455012623010202"
(- (char->integer (char-upcase c)) 65)))
(define (finish zs)
(substring (list->string (reverse
(append (list #\0 #\0 #\0) zs))) 0 4))
(let* ((cs (cdr (map char-upcase (string->list str))))
(f (string-ref str 0)) (fx (code f))
(prev (if (char=? fx #\0) #\0 fx)))
(let loop ((cs cs) (zs (list f)) (prev prev))
(if (null? cs) (finish zs)
(let ((z (code (car cs))))
(cond ((char=? z prev) (loop (cdr cs) zs prev))
((char=? z #\0) (loop (cdr cs) zs #\0))
(else (loop (cdr cs) (cons z zs) z))))))))
The code
function maps letters according to the soundex rules, with zero representing an unused character. The finish
function extends or truncates the final result to four characters. The heart of the algorithm is in the last three lines, where the current character is skipped if it maps to the same soundex code as the previous character, or if it maps to zero, or otherwise is added to the accumulating output, and the previous character is updated.
This test is due to Knuth; calling (test)
should produce no output:
(define (test)
(let ((names1 (list "Euler" "Gauss" "Hilbert"
"Knuth" "Lloyd" "Lukasiewicz"))
(names2 (list "Ellery" "Ghosh" "Heilbronn"
"Kant" "Ladd" "Lissajous"))
(result (list "E460" "G200" "H416"
"K530" "L300" "L222")))
(assert (map soundex names1) result)
(assert (map soundex names2) result)))
We used the assert
macro from the Standard Prelude. You can run the program at http://programmingpraxis.codepad.org/i0n0uIS7.
My Haskell solution (see http://bonsaicode.wordpress.com/2010/02/16/programming-praxis-soundex/ for a version with comments):
import Data.Char
import Data.List
soundex :: String -> String
soundex = f . map head . group . map toUpper where
f [] = []
f (x:xs) = x : take 3 [toNum c | c <- xs ++ repeat ‘0’,
notElem c "AEHIOUWY"]
toNum c = maybe ‘0’ snd . find (elem c . fst) $
zip (words "BFPV CGJKQSXZ DT L MN R") [‘1’..]
Hm. I think I may have accidentally used src instead of css for the language in the post above. Here’s the correct version.
It’s doesn’t return the same results for ‘lloyd’ and ‘Lukasiewicz’ but it’s close enough
[…] Praxis – Soundex By Remco Niemeijer In today’s Programming Praxis exercise we have to implement the soundex algorithm for encoding people’s […]
That Haskell up there makes me want to coo :)
http://pastebin.com/bgDF5adL
C#, I’m sure it could be faster but it works nicely.
using System;
using System.Collections.Generic;
namespace Soundex
{
class Program
{
static void Main()
{
string str = “Soundex”;
Console.WriteLine(str.Soundex());
Console.ReadKey();
}
}
static class Extensions
{
public static string Soundex(this string str)
{
char f = ”;
char last = ”;
List num;
var chr = new List(
str.ToLower().ToCharArray()
).FindAll(c =>
c >= ‘a’ && c <= 'z' && c != 'a' && c != 'e' && c != 'h' &&
c != 'i' && c != 'o' && c != 'u' && c != 'w' && c != 'y'
);
f = chr[0];
chr.RemoveAt(0);
num = new List(chr.Count);
chr.ForEach(
delegate(char c)
{
if (num.Count < 3 && c != last)
{
switch (c)
{
case 'b': case 'f':
case 'p': case 'v':
num.Add('1');
break;
case 'c': case 'g':
case 'j': case 'k':
case 'q': case 's':
case 'x': case 'z':
num.Add('2');
break;
case 'd': case 't':
num.Add('3');
break;
case 'l':
num.Add('4');
break;
case 'm': case 'n':
num.Add('5');
break;
default:
num.Add('6');
break;
}
}
last = c;
}
);
return ((f < 95) ? f : (char)(f – 32)) + new string(num.ToArray()).PadRight(3, '0');
}
}
}
Python version.
_code is a mapping of letter to soundex code or “” for letters in the first group.
The algorithm doesn’t say how to handle punctuation (spaces, hyphens, apostrophes), so I strip them.
groupby processes its first argument to find groups of items having the same key, as determined by the second argument. _code.get does a dictionary lookup, returning the soundex code. So, groupby returns (k,v)-pairs, where k is a soundex code, and v is a group of adjacent letters having that code. The join concatentates the soundex codes, skipping the first.
My implementation in C