1// Package soundex implements the Soundex phonetic algorithm as a pure,2// reusable package: names that sound alike in English encode to the same3// 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 the6// rules that are usually got wrong implemented explicitly:7//8// - the first letter is kept as-is and its digit still suppresses a9// following consonant of the same code ("Pfister" → P236, not P123);10// - 'h' and 'w' are TRANSPARENT: consonants either side of them are treated11// as adjacent ("Ashcraft" → A261, not A226);12// - vowels are not transparent — they separate, so a repeated code after a13// vowel is emitted again ("Tymczak" → T522).14//15// Soundex is English-centric and lossy by design: it is a *blocking* key for16// finding candidates, never a proof that two names match.17//18// A live demo of this package is at19// [r/moul/x/daily/soundexdemo](/r/moul/x/daily/soundexdemo/v0).20package soundex2122import "strings"2324// 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 041}4243// 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 -= 3247 }48 if c >= 'A' && c <= 'Z' {49 return c50 }51 return 052}5354// Encode returns the four-character Soundex key for s, or "" when s contains55// no ASCII letters. Non-letters are ignored, so "O'Brien" and "OBrien" agree.56func Encode(s string) string {57 // keep letters only58 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 }6768 out := []byte{letters[0]}69 prev := code(letters[0]) // the first letter's own code still suppresses7071 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 consonants77 // 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 = 081 }82 case d != prev:83 out = append(out, d)84 prev = d85 default:86 // same code as the previous consonant, and not separated by a87 // vowel: collapsed.88 }89 }90 for len(out) < 4 {91 out = append(out, '0')92 }93 return string(out)94}9596// Match reports whether two strings share a Soundex key. Empty keys never97// 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 == kb101}102103// 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 out110}111112// Normalize upper-cases and strips non-letters — the input Encode actually113// sees. Exposed so callers can show their work.114func Normalize(s string) string {115 var b strings.Builder116 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}123Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.