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

soundex.gno

3.30 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/moul/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}