Algorithm Search⌘K

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
soundex.pyRawGitHub
# 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

Read the full page about Soundex