Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

soundex.gno

3.32 Kb · 122 lines
  1// Package soundex implements the Soundex phonetic algorithm as a pure,
  2// reusable package: names that sound alike in English encode to the same
  3// four-character key, so "Robert" and "Rupert" both give R163.
  4//
  5// This is the 1918 Russell/Odell algorithm as used by the US census, with the
  6// rules that are usually got wrong implemented explicitly:
  7//
  8//   - the first letter is kept as-is and its digit still suppresses a
  9//     following consonant of the same code ("Pfister" → P236, not P123);
 10//   - 'h' and 'w' are TRANSPARENT: consonants either side of them are treated
 11//     as adjacent ("Ashcraft" → A261, not A226);
 12//   - vowels are not transparent — they separate, so a repeated code after a
 13//     vowel is emitted again ("Tymczak" → T522).
 14//
 15// Soundex is English-centric and lossy by design: it is a *blocking* key for
 16// finding candidates, never a proof that two names match.
 17//
 18// A live demo of this package is at
 19// [r/moul/x/daily/soundexdemo](/r/nym-moultest123/x/daily/soundexdemo/v0).
 20package soundex
 21
 22import "strings"
 23
 24// code returns the Soundex digit for a letter, or 0 for vowels and h/w/y.
 25func code(c byte) byte {
 26	switch c {
 27	case 'B', 'F', 'P', 'V':
 28		return '1'
 29	case 'C', 'G', 'J', 'K', 'Q', 'S', 'X', 'Z':
 30		return '2'
 31	case 'D', 'T':
 32		return '3'
 33	case 'L':
 34		return '4'
 35	case 'M', 'N':
 36		return '5'
 37	case 'R':
 38		return '6'
 39	}
 40	return 0
 41}
 42
 43// upper returns c upper-cased if it is an ASCII letter, else 0.
 44func upper(c byte) byte {
 45	if c >= 'a' && c <= 'z' {
 46		c -= 32
 47	}
 48	if c >= 'A' && c <= 'Z' {
 49		return c
 50	}
 51	return 0
 52}
 53
 54// Encode returns the four-character Soundex key for s, or "" when s contains
 55// no ASCII letters. Non-letters are ignored, so "O'Brien" and "OBrien" agree.
 56func Encode(s string) string {
 57	// keep letters only
 58	letters := make([]byte, 0, len(s))
 59	for i := 0; i < len(s); i++ {
 60		if c := upper(s[i]); c != 0 {
 61			letters = append(letters, c)
 62		}
 63	}
 64	if len(letters) == 0 {
 65		return ""
 66	}
 67
 68	out := []byte{letters[0]}
 69	prev := code(letters[0]) // the first letter's own code still suppresses
 70
 71	for i := 1; i < len(letters) && len(out) < 4; i++ {
 72		c := letters[i]
 73		d := code(c)
 74		switch {
 75		case d == 0:
 76			// 'h' and 'w' are transparent: leave prev alone so the consonants
 77			// either side are treated as adjacent. Vowels (and 'y') separate,
 78			// so they reset prev and allow the same code to repeat.
 79			if c != 'H' && c != 'W' {
 80				prev = 0
 81			}
 82		case d != prev:
 83			out = append(out, d)
 84			prev = d
 85		default:
 86			// same code as the previous consonant, and not separated by a
 87			// vowel: collapsed.
 88		}
 89	}
 90	for len(out) < 4 {
 91		out = append(out, '0')
 92	}
 93	return string(out)
 94}
 95
 96// Match reports whether two strings share a Soundex key. Empty keys never
 97// match, so two inputs with no letters are not "the same name".
 98func Match(a, b string) bool {
 99	ka, kb := Encode(a), Encode(b)
100	return ka != "" && ka == kb
101}
102
103// EncodeAll returns the keys for each input, in order.
104func EncodeAll(names []string) []string {
105	out := make([]string, 0, len(names))
106	for _, n := range names {
107		out = append(out, Encode(n))
108	}
109	return out
110}
111
112// Normalize upper-cases and strips non-letters — the input Encode actually
113// sees. Exposed so callers can show their work.
114func Normalize(s string) string {
115	var b strings.Builder
116	for i := 0; i < len(s); i++ {
117		if c := upper(s[i]); c != 0 {
118			b.WriteByte(c)
119		}
120	}
121	return b.String()
122}