// sample-wordcount.go: counts words in text, including non-ASCII text. // Free sample from 99helpers.com/file-types/go // // Its tests are in sample-wordcount_test.go. Keep both in the same folder: // // go run sample-wordcount.go // go test -v sample-wordcount.go sample-wordcount_test.go package main import ( "cmp" "fmt" "slices" "strings" "unicode" "unicode/utf8" ) // WordCount is one word and how often it appears. type WordCount struct { Word string Count int } // CountWords splits text into words (runs of letters and digits), folds // them to lower case and counts each one. Go strings are UTF-8, and // unicode.IsLetter works for every script, so "Café" and "東京" are words. func CountWords(text string) map[string]int { counts := make(map[string]int) words := strings.FieldsFunc(text, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) for _, w := range words { counts[strings.ToLower(w)]++ } return counts } // TopWords returns the n most frequent words, most frequent first, with // ties in alphabetical order so the result is always the same. func TopWords(counts map[string]int, n int) []WordCount { list := make([]WordCount, 0, len(counts)) for w, c := range counts { list = append(list, WordCount{w, c}) } slices.SortFunc(list, func(a, b WordCount) int { if c := cmp.Compare(b.Count, a.Count); c != 0 { return c } return strings.Compare(a.Word, b.Word) }) return list[:min(n, len(list))] } func main() { text := "The café opens at nine. The CAFÉ closes at six; café au lait costs €3. " + "Der Bäcker backt, der Bäcker verkauft. 東京 東京 大阪" for _, wc := range TopWords(CountWords(text), 5) { // len counts bytes; utf8.RuneCountInString counts characters. fmt.Printf("%-8s %d (%d bytes, %d runes)\n", wc.Word, wc.Count, len(wc.Word), utf8.RuneCountInString(wc.Word)) } }