← run

go-02-word-frequency

1.000
2/2 tests· data
Challenge · difficulty 2/5
# Word frequency

Implement **`solution.go`** in `package challenge` exporting:

```go
func WordFrequency(text string) map[string]int
```

Count how many times each word occurs in `text` and return the counts in a map.

Rules:

- Split `text` into tokens on **whitespace** (spaces, tabs, newlines).
- For each token, strip any **surrounding ASCII punctuation** (leading and trailing).
  Punctuation in the middle of a token is kept.
- **Lowercase** each word before counting.
- If, after stripping, a token is empty, skip it (do not count an empty string).
- For empty or whitespace-only input, return an **empty, non-nil** map (length 0).

ASCII punctuation is the set of characters for which Go's `unicode.IsPunct` returns true
together with symbols such as `+`, `<`, `=`, etc. For this challenge, treat a byte as
"punctuation to strip" when it is an ASCII byte that is **not** a letter or digit.

Examples:

- `WordFrequency("the cat sat on the mat")` →
  `{"the": 2, "cat": 1, "sat": 1, "on": 1, "mat": 1}`
- `WordFrequency("Hello, hello! HELLO.")` → `{"hello": 3}`
- `WordFrequency("don't stop")` → `{"don't": 1, "stop": 1}` (interior apostrophe kept)
- `WordFrequency("   ")` → `{}` (empty, non-nil map)
tests/solution_test.go
package challenge

import (
	"reflect"
	"testing"
)

func TestWordFrequency(t *testing.T) {
	cases := []struct {
		name string
		in   string
		want map[string]int
	}{
		{
			name: "simple repeats",
			in:   "the cat sat on the mat",
			want: map[string]int{"the": 2, "cat": 1, "sat": 1, "on": 1, "mat": 1},
		},
		{
			name: "punctuation and case",
			in:   "Hello, hello! HELLO.",
			want: map[string]int{"hello": 3},
		},
		{
			name: "interior apostrophe kept",
			in:   "don't stop don't",
			want: map[string]int{"don't": 2, "stop": 1},
		},
		{
			name: "tabs and newlines as whitespace",
			in:   "a\tb\nc a",
			want: map[string]int{"a": 2, "b": 1, "c": 1},
		},
		{
			name: "leading and trailing punctuation stripped",
			in:   "(go) [go]; {GO}",
			want: map[string]int{"go": 3},
		},
		{
			name: "digits are words",
			in:   "42 42 forty-two",
			want: map[string]int{"42": 2, "forty-two": 1},
		},
		{
			name: "token that is only punctuation is skipped",
			in:   "hi --- !!! bye",
			want: map[string]int{"hi": 1, "bye": 1},
		},
		{
			name: "empty input",
			in:   "",
			want: map[string]int{},
		},
		{
			name: "whitespace only",
			in:   "   \t \n  ",
			want: map[string]int{},
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got := WordFrequency(c.in)
			if got == nil {
				t.Fatalf("WordFrequency(%q) returned nil map, want non-nil", c.in)
			}
			if !reflect.DeepEqual(got, c.want) {
				t.Errorf("WordFrequency(%q) = %v, want %v", c.in, got, c.want)
			}
		})
	}
}

func TestWordFrequencyEmptyIsNonNil(t *testing.T) {
	got := WordFrequency("")
	if got == nil {
		t.Fatal("WordFrequency(\"\") = nil, want non-nil empty map")
	}
	if len(got) != 0 {
		t.Errorf("WordFrequency(\"\") = %v, want empty map", got)
	}
}
Proposed solution
```go
package challenge

import "strings"

func WordFrequency(text string) map[string]int {
	counts := make(map[string]int)
	words := strings.Fields(text)
	for _, w := range words {
		start := 0
		for start < len(w) {
			b := w[start]
			if b < 128 && !isASCIIAlnum(b) {
				start++
			} else {
				break
			}
		}
		end := len(w)
		for end > start {
			b := w[end-1]
			if b < 128 && !isASCIIAlnum(b) {
				end--
			} else {
				break
			}
		}
		cleaned := w[start:end]
		if cleaned == "" {
			continue
		}
		counts[strings.ToLower(cleaned)]++
	}
	return counts
}

func isASCIIAlnum(b byte) bool {
	return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
}
```

Test output (stdout)

ipped","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852498803+02:00","Action":"run","Package":"challenge","Test":"TestWordFrequency/digits_are_words"}
{"Time":"2026-06-29T19:35:13.852499663+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/digits_are_words","Output":"=== RUN   TestWordFrequency/digits_are_words\n"}
{"Time":"2026-06-29T19:35:13.852500763+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/digits_are_words","Output":"--- PASS: TestWordFrequency/digits_are_words (0.00s)\n"}
{"Time":"2026-06-29T19:35:13.852501623+02:00","Action":"pass","Package":"challenge","Test":"TestWordFrequency/digits_are_words","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852502753+02:00","Action":"run","Package":"challenge","Test":"TestWordFrequency/token_that_is_only_punctuation_is_skipped"}
{"Time":"2026-06-29T19:35:13.852503523+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/token_that_is_only_punctuation_is_skipped","Output":"=== RUN   TestWordFrequency/token_that_is_only_punctuation_is_skipped\n"}
{"Time":"2026-06-29T19:35:13.852504603+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/token_that_is_only_punctuation_is_skipped","Output":"--- PASS: TestWordFrequency/token_that_is_only_punctuation_is_skipped (0.00s)\n"}
{"Time":"2026-06-29T19:35:13.852505643+02:00","Action":"pass","Package":"challenge","Test":"TestWordFrequency/token_that_is_only_punctuation_is_skipped","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852506533+02:00","Action":"run","Package":"challenge","Test":"TestWordFrequency/empty_input"}
{"Time":"2026-06-29T19:35:13.852507323+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/empty_input","Output":"=== RUN   TestWordFrequency/empty_input\n"}
{"Time":"2026-06-29T19:35:13.852508283+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/empty_input","Output":"--- PASS: TestWordFrequency/empty_input (0.00s)\n"}
{"Time":"2026-06-29T19:35:13.852509143+02:00","Action":"pass","Package":"challenge","Test":"TestWordFrequency/empty_input","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852510893+02:00","Action":"run","Package":"challenge","Test":"TestWordFrequency/whitespace_only"}
{"Time":"2026-06-29T19:35:13.852512253+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/whitespace_only","Output":"=== RUN   TestWordFrequency/whitespace_only\n"}
{"Time":"2026-06-29T19:35:13.852513333+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency/whitespace_only","Output":"--- PASS: TestWordFrequency/whitespace_only (0.00s)\n"}
{"Time":"2026-06-29T19:35:13.852514793+02:00","Action":"pass","Package":"challenge","Test":"TestWordFrequency/whitespace_only","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852515893+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequency","Output":"--- PASS: TestWordFrequency (0.00s)\n"}
{"Time":"2026-06-29T19:35:13.852516943+02:00","Action":"pass","Package":"challenge","Test":"TestWordFrequency","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852518633+02:00","Action":"run","Package":"challenge","Test":"TestWordFrequencyEmptyIsNonNil"}
{"Time":"2026-06-29T19:35:13.852519653+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequencyEmptyIsNonNil","Output":"=== RUN   TestWordFrequencyEmptyIsNonNil\n"}
{"Time":"2026-06-29T19:35:13.852520863+02:00","Action":"output","Package":"challenge","Test":"TestWordFrequencyEmptyIsNonNil","Output":"--- PASS: TestWordFrequencyEmptyIsNonNil (0.00s)\n"}
{"Time":"2026-06-29T19:35:13.852521833+02:00","Action":"pass","Package":"challenge","Test":"TestWordFrequencyEmptyIsNonNil","Elapsed":0}
{"Time":"2026-06-29T19:35:13.852522943+02:00","Action":"output","Package":"challenge","Output":"PASS\n"}
{"Time":"2026-06-29T19:35:13.85271393+02:00","Action":"output","Package":"challenge","Output":"ok  \tchallenge\t0.001s\n"}
{"Time":"2026-06-29T19:35:13.85273415+02:00","Action":"pass","Package":"challenge","Elapsed":0.001}