Soundex in Python
Phonetic encoding that maps a name to a letter plus three digits.
- File
soundex.py, 38 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/soundex/. The authors are listed in prov/8WZ6WZ.jsonl.
"""American Soundex code of a name: one letter and three digits."""
_CODE = {}
for _letters, _digit in (("BFPV", "1"), ("CGJKQSXZ", "2"), ("DT", "3"), ("L", "4"), ("MN", "5"), ("R", "6")):
for _ch in _letters:
_CODE[_ch] = _digit
def code(ch: str) -> str:
"""Digit of an upper-case letter; "0" for A, E, I, O, U, Y, H and W."""
return _CODE.get(ch, "0")
def soundex(name: str) -> str:
"""Return the Soundex code of name, for example "R163" for "Robert".
Characters that are not ASCII letters are ignored, and case does not
matter. Precondition: name holds at least one ASCII letter, else
ValueError. Cost: O(len(name)) time.
"""
s = [c for c in name.upper() if "A" <= c <= "Z"] # @line L1
if not s:
raise ValueError("the name holds no letter")
out = [s[0]] # @line L2
last = code(s[0])
for ch in s[1:]: # @line L3
c = code(ch) # @line L4
if ch in "HW": # @line L5
continue
if c != "0" and c != last: # @line L6
out.append(c)
if len(out) == 4: # @line L8
break
last = c # @line L7
return "".join(out).ljust(4, "0") # @line L9
test_soundex.py 56 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/soundex/. The authors are listed in prov/8WZ6WZ.jsonl.
import random
import pytest
from soundex import code, soundex
def rules(name: str) -> str:
"""The four rules of the definition, applied one after the other."""
s = [c for c in name.upper() if "A" <= c <= "Z"]
keep = []
for i in range(1, len(s)):
if s[i] in "AEIOUYHW":
continue
j = i - 1
while j > 0 and s[j] in "HW":
j -= 1
if code(s[j]) == code(s[i]):
continue
keep.append(code(s[i]))
return (s[0] + "".join(keep) + "000")[:4]
def test_examples() -> None:
table = {
"Robert": "R163", "Rupert": "R163", "Rubin": "R150", "Ashcraft": "A261",
"Ashcroft": "A261", "Tymczak": "T522", "Pfister": "P236", "Honeyman": "H555",
"Lee": "L000", "Euler": "E460", "Ellery": "E460", "Gauss": "G200", "Ghosh": "G200",
"Hilbert": "H416", "Heilbronn": "H416", "Knuth": "K530", "Kant": "K530",
"Lloyd": "L300", "Ladd": "L300", "Lukasiewicz": "L222", "Lissajous": "L222",
}
for name, want in table.items():
assert soundex(name) == want
assert rules(name) == want
def test_edges() -> None:
assert soundex("a") == "A000"
assert soundex("o'Hara") == "O600"
assert soundex(" van Dyke ") == soundex("VANDYKE")
with pytest.raises(ValueError):
soundex("")
with pytest.raises(ValueError):
soundex("123 -")
def test_random_against_rules() -> None:
rng = random.Random(1918)
for letters in ("BH", "AEBP", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "HWSCAZ"):
for _ in range(600):
name = "".join(rng.choice(letters) for _ in range(rng.randint(1, 12)))
got = soundex(name)
assert got == rules(name)
assert len(got) == 4 and got[0] == name[0] and got[1:].isdigit()
What the next level needs
- the tests did not run in this build