From 5bd4222342227a5c54b5368ae0586292c3c9cf2d Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Wed, 19 Jun 2024 10:55:35 -0400 Subject: [PATCH] Finished maps --- learn_go_with_tests/my_maps/README.md | 22 ++++ learn_go_with_tests/my_maps/dictionary.go | 74 ++++++++++++ .../my_maps/dictionary_test.go | 111 ++++++++++++++++++ learn_go_with_tests/my_maps/go.mod | 3 + 4 files changed, 210 insertions(+) create mode 100644 learn_go_with_tests/my_maps/README.md create mode 100644 learn_go_with_tests/my_maps/dictionary.go create mode 100644 learn_go_with_tests/my_maps/dictionary_test.go create mode 100644 learn_go_with_tests/my_maps/go.mod diff --git a/learn_go_with_tests/my_maps/README.md b/learn_go_with_tests/my_maps/README.md new file mode 100644 index 0000000..a0f5102 --- /dev/null +++ b/learn_go_with_tests/my_maps/README.md @@ -0,0 +1,22 @@ +# Maps + +https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/maps + +Build our own map + + +## Gotcha + +A gotcha with maps is that they can be a nil value. A nil map behaves like an empty map when reading, but attempts to write to a nil map will cause a runtime panic. You can read more about maps here. + +Therefore, you should never initialize an empty map variable: + +var m map[string]string + +Instead, you can initialize an empty map like we were doing above, or use the make keyword to create a map for you: + +var dictionary = map[string]string{} + +// OR + +var dictionary = make(map[string]string) \ No newline at end of file diff --git a/learn_go_with_tests/my_maps/dictionary.go b/learn_go_with_tests/my_maps/dictionary.go new file mode 100644 index 0000000..50d078b --- /dev/null +++ b/learn_go_with_tests/my_maps/dictionary.go @@ -0,0 +1,74 @@ +package main + +// these are sentinel errors. Look at article in pointers_errors dictory README.md +// var ( +// ErrNotFound = errors.New("could not find word you were looking for") +// ErrWordExists = errors.New("key already exists, Add failed") +// ) + +// Instead we implement our own type that implements the error interface +// This has more benefits because you can do type checks instead of comparing error strings +// or sentinel errors. See the article https://dave.cheney.net/2016/04/07/constant-errors +const ( + ErrNotFound = DictionaryErr("could not find word you were looking for") + ErrWordExists = DictionaryErr("key already exists, Add failed") + ErrWordDoesNotExist = DictionaryErr("key already exists, Add failed") +) + +type DictionaryErr string + +func (e DictionaryErr) Error() string { + return string(e) +} + +type Dictionary map[string]string + +func (d Dictionary) Search(word string) (string, error) { + val, ok := d[word] + if !ok { + return val, ErrNotFound + } + + return val, nil +} + +// Ok...this works because a map value is a pointer to a runtime.hmap structure +// So when you pass the map reference to this function it's actually copying the +// pointer so no need to &d get mem pointer. +func (d Dictionary) Add(word, definition string) error { + _, ok := d[word] + if ok { + return ErrWordExists + } + + d[word] = definition + return nil + + // The authors alt implementation + // _, err := d.Search(word) + + // switch err { + // case ErrNotFound: + // d[word] = definition + // case nil: + // return ErrWordExists + // default: + // return err + // } + + // return nil +} + +func (d Dictionary) Update(word, definition string) error { + _, ok := d[word] + if !ok { + return ErrWordDoesNotExist + } + d[word] = definition + return nil +} + +func (d Dictionary) Delete(word string) { + //Go has a built-in function delete that works on maps. It takes two arguments. The first is the map and the second is the key to be removed. + delete(d, word) +} diff --git a/learn_go_with_tests/my_maps/dictionary_test.go b/learn_go_with_tests/my_maps/dictionary_test.go new file mode 100644 index 0000000..5179888 --- /dev/null +++ b/learn_go_with_tests/my_maps/dictionary_test.go @@ -0,0 +1,111 @@ +package main + +import ( + "testing" +) + +func assertError(t testing.TB, got, want error) { + t.Helper() + if got != want { + t.Errorf("got error %q want %q", got, want) + } +} + +func assertStrings(t testing.TB, got, want string) { + t.Helper() + if got != want { + t.Errorf("got %q want %q give %q", got, want, "test") + } +} + +func TestSearch(t *testing.T) { + searchStr := "this is just a test" + // dictionary := map[string]string{"test": searchStr} + // use our own type + dictionary := Dictionary{"test": searchStr} + + t.Run("known word", func(t *testing.T) { + got, _ := dictionary.Search("test") + want := searchStr + + assertStrings(t, got, want) + }) + + t.Run("unknown word", func(t *testing.T) { + _, err := dictionary.Search("not-test") + + assertError(t, err, ErrNotFound) + + // testing for an error's string is crude since error str can be modified + // see README.md pointers_errors directory + assertStrings(t, err.Error(), ErrNotFound.Error()) + }) + +} + +func TestAdd(t *testing.T) { + + t.Run("new word", func(t *testing.T) { + dictionary := Dictionary{} + word := "test" + definition := "this is just a test" + + dictionary.Add(word, definition) + + assertDefinition(t, dictionary, word, definition) + + }) + + t.Run("new word", func(t *testing.T) { + word := "test" + definition := "this is just a test" + dictionary := Dictionary{word: definition} + + err := dictionary.Add(word, definition) + + assertError(t, err, ErrWordExists) + assertDefinition(t, dictionary, word, definition) + }) + +} + +func assertDefinition(t testing.TB, dictionary Dictionary, word, definition string) { + t.Helper() + + got, err := dictionary.Search(word) + if err != nil { + t.Fatal("should find added word:", err) + } + assertStrings(t, got, definition) + +} + +func TestUpdate(t *testing.T) { + updateStr := "Don't build the Torment Nexus!" + + t.Run("update works", func(t *testing.T) { + dictionary := Dictionary{"test": "this is just a test"} + + dictionary.Update("test", updateStr) + + assertDefinition(t, dictionary, "test", updateStr) + }) + + t.Run("key does not exist", func(t *testing.T) { + dictionary := Dictionary{} + + err := dictionary.Update("test", updateStr) + + assertError(t, err, ErrWordDoesNotExist) + }) +} + +func TestDelete(t *testing.T) { + dictionary := Dictionary{"test": "Don't build the Torment Nexus!"} + + dictionary.Delete("test") + + _, err := dictionary.Search("test") + + assertError(t, err, ErrNotFound) +} diff --git a/learn_go_with_tests/my_maps/go.mod b/learn_go_with_tests/my_maps/go.mod new file mode 100644 index 0000000..b490d1f --- /dev/null +++ b/learn_go_with_tests/my_maps/go.mod @@ -0,0 +1,3 @@ +module my_maps + +go 1.21.0