From 828f289f3c420441c397081eb43e750560283ad7 Mon Sep 17 00:00:00 2001 From: mitch Date: Wed, 19 Jan 2022 23:20:12 -0500 Subject: [PATCH] First Commit --- .gitignore | 105 ++++++++++++++++++ adder.go | 33 ++++++ adder_test.go | 65 +++++++++++ clockface.go | 34 ++++++ clockface/clockface.go | 1 + clockface/clockface/main.go | 34 ++++++ clockface/clockface_acceptance_test.go | 28 +++++ clockface/clockface_test.go | 65 +++++++++++ clockface_test.go | 87 +++++++++++++++ concurrency.go | 25 +++++ concurrency_test.go | 50 +++++++++ context.go | 22 ++++ context_test.go | 95 ++++++++++++++++ di.go | 14 +++ di_test.go | 18 +++ dictionary.go | 53 +++++++++ dictionary_test.go | 105 ++++++++++++++++++ fintech.go | 38 +++++++ fintech_test.go | 53 +++++++++ go.mod | 3 + hello.go | 34 ++++++ hello_test.go | 35 ++++++ mocking.go | 45 ++++++++ mocking_test.go | 95 ++++++++++++++++ reflection.go | 57 ++++++++++ reflection_test.go | 148 +++++++++++++++++++++++++ repeat.go | 9 ++ repeat_test.go | 18 +++ roman_numerals.go | 90 +++++++++++++++ roman_numerals_test.go | 84 ++++++++++++++ select.go | 39 +++++++ select_test.go | 47 ++++++++ shapes.go | 37 +++++++ shapes_test.go | 33 ++++++ sync.go | 22 ++++ sync_test.go | 42 +++++++ 36 files changed, 1763 insertions(+) create mode 100644 .gitignore create mode 100644 adder.go create mode 100644 adder_test.go create mode 100644 clockface.go create mode 100644 clockface/clockface.go create mode 100644 clockface/clockface/main.go create mode 100644 clockface/clockface_acceptance_test.go create mode 100644 clockface/clockface_test.go create mode 100644 clockface_test.go create mode 100644 concurrency.go create mode 100644 concurrency_test.go create mode 100644 context.go create mode 100644 context_test.go create mode 100644 di.go create mode 100644 di_test.go create mode 100644 dictionary.go create mode 100644 dictionary_test.go create mode 100644 fintech.go create mode 100644 fintech_test.go create mode 100644 go.mod create mode 100644 hello.go create mode 100644 hello_test.go create mode 100644 mocking.go create mode 100644 mocking_test.go create mode 100644 reflection.go create mode 100644 reflection_test.go create mode 100644 repeat.go create mode 100644 repeat_test.go create mode 100644 roman_numerals.go create mode 100644 roman_numerals_test.go create mode 100644 select.go create mode 100644 select_test.go create mode 100644 shapes.go create mode 100644 shapes_test.go create mode 100644 sync.go create mode 100644 sync_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..085f9fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig + +# Created by https://www.gitignore.io/api/macos,visualstudiocode,terraform,windows +# Edit at https://www.gitignore.io/?templates=macos,visualstudiocode,terraform,windows + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* +*.plan + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json +secrets.tfvars + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +### VisualStudioCode ### +.vscode/* +#!.vscode/settings.json +#!.vscode/tasks.json +#!.vscode/launch.json +#!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/macos,visualstudiocode,terraform,windows + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) +.vscode/* + +.idea \ No newline at end of file diff --git a/adder.go b/adder.go new file mode 100644 index 0000000..c3c27c3 --- /dev/null +++ b/adder.go @@ -0,0 +1,33 @@ +package main + +//func main() { +//} + +func Add(numbers []int) int { + var sum int + for _, num := range numbers { + sum += num + } + return sum +} + +func SumAll(numbersToSum ...[]int) []int { + var sums []int + for _, numbers := range numbersToSum { + sums = append(sums, Add(numbers)) + } + return sums +} + +func SumAllTails(numbersToSum ...[]int) []int { + var sums []int + for _, numbers := range numbersToSum { + if len(numbers) == 0 { + sums = append(sums, 0) + } else { + tail := numbers[1:] + sums = append(sums, Add(tail)) + } + } + return sums +} diff --git a/adder_test.go b/adder_test.go new file mode 100644 index 0000000..5338f60 --- /dev/null +++ b/adder_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestAdder(t *testing.T) { + assertCorrectMessage := func(t testing.TB, sum, expect int) { + if sum != expect { + t.Errorf("expected %d but got %d", sum, expect) + } + } + + t.Run("2 + 2", func(t *testing.T) { + numbers := []int{2, 2} + sum := Add(numbers) + expect := 4 + assertCorrectMessage(t, sum, expect) + }) + + t.Run("1 + 1", func(t *testing.T) { + numbers := []int{1, 1} + sum := Add(numbers) + expect := 2 + assertCorrectMessage(t, sum, expect) + }) + + t.Run("1 + 2 + 3 + 4 + 5", func(t *testing.T) { + numbers := []int{1, 2, 3, 4, 5} + sum := Add(numbers) + expect := 15 + assertCorrectMessage(t, sum, expect) + }) +} + +func TestAdderSlice(t *testing.T) { + got := SumAll([]int{1, 2}, []int{0, 9}) + want := []int{3, 9} + + // this is not type safe + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v want %v", got, want) + } +} + +func TestSumAllTails(t *testing.T) { + checkSums := func(t testing.TB, got, want []int) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v want %v", got, want) + } + } + t.Run("make the some of some slices", func(t *testing.T) { + got := SumAllTails([]int{1, 2}, []int{0, 9}) + want := []int{2, 9} + checkSums(t, got, want) + }) + + t.Run("safely sum empty slices", func(t *testing.T) { + got := SumAllTails([]int{}, []int{3, 4, 5}) + want := []int{0, 9} + checkSums(t, got, want) + }) +} diff --git a/clockface.go b/clockface.go new file mode 100644 index 0000000..fe94789 --- /dev/null +++ b/clockface.go @@ -0,0 +1,34 @@ +package main + +import ( + "math" + "time" +) + +const secondHandLength = 90 +const clockCentreX = 150 +const clockCentreY = 150 + +type Point struct { + X float64 + Y float64 +} + +func SecondHand(t time.Time) Point { + p := secondHandPoint(t) + p = Point{p.X * secondHandLength, p.Y * secondHandLength} //scale + p = Point{p.X, -p.Y} //flip + p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate + return p +} + +func secondsInRadians(t time.Time) float64 { + return math.Pi / (30 / (float64(t.Second()))) +} + +func secondHandPoint(t time.Time) Point { + angle := secondsInRadians(t) + x := math.Sin(angle) + y := math.Cos(angle) + return Point{x, y} +} diff --git a/clockface/clockface.go b/clockface/clockface.go new file mode 100644 index 0000000..1c65078 --- /dev/null +++ b/clockface/clockface.go @@ -0,0 +1 @@ +package clockface diff --git a/clockface/clockface/main.go b/clockface/clockface/main.go new file mode 100644 index 0000000..b3bbe2c --- /dev/null +++ b/clockface/clockface/main.go @@ -0,0 +1,34 @@ +package clockface + +import ( + "math" + "time" +) + +const secondHandLength = 90 +const clockCentreX = 150 +const clockCentreY = 150 + +type Point struct { + X float64 + Y float64 +} + +func SecondHand(t time.Time) Point { + p := secondHandPoint(t) + p = Point{p.X * secondHandLength, p.Y * secondHandLength} //scale + p = Point{p.X, -p.Y} //flip + p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate + return p +} + +func secondsInRadians(t time.Time) float64 { + return math.Pi / (30 / (float64(t.Second()))) +} + +func secondHandPoint(t time.Time) Point { + angle := secondsInRadians(t) + x := math.Sin(angle) + y := math.Cos(angle) + return Point{x, y} +} diff --git a/clockface/clockface_acceptance_test.go b/clockface/clockface_acceptance_test.go new file mode 100644 index 0000000..a47d890 --- /dev/null +++ b/clockface/clockface_acceptance_test.go @@ -0,0 +1,28 @@ +package clockface + +import ( + "testing" + "time" +) + +func TestSecondHandAtMidnight(t *testing.T) { + tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) + + want := Point{X: 150, Y: 150 - 90} + got := SecondHand(tm) + + if got != want { + t.Errorf("Got %v, want %v", got, want) + } +} + +func TestSecondHandAt30Seconds(t *testing.T) { + tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) + + want := Point{X: 150, Y: 150 + 90} + got := SecondHand(tm) + + if got != want { + t.Errorf("Got %v, wanted %v", got, want) + } +} diff --git a/clockface/clockface_test.go b/clockface/clockface_test.go new file mode 100644 index 0000000..7104169 --- /dev/null +++ b/clockface/clockface_test.go @@ -0,0 +1,65 @@ +package clockface + +import ( + "math" + "testing" + "time" +) + +func simpleTime(hours, minutes, seconds int) time.Time { + return time.Date(312, time.October, 28, hours, minutes, seconds, 0, time.UTC) +} + +func testName(t time.Time) string { + return t.Format("15:04:05") +} + +func TestSecondsInRadians(t *testing.T) { + cases := []struct { + time time.Time + angle float64 + }{ + {simpleTime(0, 0, 30), math.Pi}, + {simpleTime(0, 0, 0), 0}, + {simpleTime(0, 0, 45), (math.Pi / 2) * 3}, + {simpleTime(0, 0, 7), (math.Pi / 30) * 7}, + } + + for _, c := range cases { + t.Run(testName(c.time), func(t *testing.T) { + got := secondsInRadians(c.time) + if !roughlyEqualFloat64(got, c.angle) { + t.Fatalf("Wanted %v radians, but got %v", c.angle, got) + } + }) + } +} + +func TestSecondHandVector(t *testing.T) { + cases := []struct { + time time.Time + point Point + }{ + {simpleTime(0, 0, 30), Point{0, -1}}, + {simpleTime(0, 0, 45), Point{-1, 0}}, + } + + for _, c := range cases { + t.Run(testName(c.time), func(t *testing.T) { + got := secondHandPoint(c.time) + if !roughlyEqualPoint(got, c.point) { + t.Fatalf("Wanted %v Point, but got %v", c.point, got) + } + }) + } +} + +func roughlyEqualFloat64(a, b float64) bool { + const equalityThreshold = 1e-7 + return math.Abs(a-b) < equalityThreshold +} + +func roughlyEqualPoint(a, b Point) bool { + return roughlyEqualFloat64(a.X, b.X) && + roughlyEqualFloat64(a.Y, b.Y) +} diff --git a/clockface_test.go b/clockface_test.go new file mode 100644 index 0000000..547836a --- /dev/null +++ b/clockface_test.go @@ -0,0 +1,87 @@ +package main + +import ( + "math" + "testing" + "time" +) + +func simpleTime(hours, minutes, seconds int) time.Time { + return time.Date(312, time.October, 28, hours, minutes, seconds, 0, time.UTC) +} + +func testName(t time.Time) string { + return t.Format("15:04:05") +} + +func TestSecondsInRadians(t *testing.T) { + cases := []struct { + time time.Time + angle float64 + }{ + {simpleTime(0, 0, 30), math.Pi}, + {simpleTime(0, 0, 0), 0}, + {simpleTime(0, 0, 45), (math.Pi / 2) * 3}, + {simpleTime(0, 0, 7), (math.Pi / 30) * 7}, + } + + for _, c := range cases { + t.Run(testName(c.time), func(t *testing.T) { + got := secondsInRadians(c.time) + if !roughlyEqualFloat64(got, c.angle) { + t.Fatalf("Wanted %v radians, but got %v", c.angle, got) + } + }) + } +} + +func TestSecondHandVector(t *testing.T) { + cases := []struct { + time time.Time + point Point + }{ + {simpleTime(0, 0, 30), Point{0, -1}}, + {simpleTime(0, 0, 45), Point{-1, 0}}, + } + + for _, c := range cases { + t.Run(testName(c.time), func(t *testing.T) { + got := secondHandPoint(c.time) + if !roughlyEqualPoint(got, c.point) { + t.Fatalf("Wanted %v Point, but got %v", c.point, got) + } + }) + } +} + +func roughlyEqualFloat64(a, b float64) bool { + const equalityThreshold = 1e-7 + return math.Abs(a-b) < equalityThreshold +} + +func roughlyEqualPoint(a, b Point) bool { + return roughlyEqualFloat64(a.X, b.X) && + roughlyEqualFloat64(a.Y, b.Y) +} + +func TestSecondHandAtMidnight(t *testing.T) { + tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC) + + want := Point{X: 150, Y: 150 - 90} + got := SecondHand(tm) + + if got != want { + t.Errorf("Got %v, want %v", got, want) + } +} + +func TestSecondHandAt30Seconds(t *testing.T) { + tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC) + + want := Point{X: 150, Y: 150 + 90} + got := SecondHand(tm) + + if got != want { + t.Errorf("Got %v, wanted %v", got, want) + } +} diff --git a/concurrency.go b/concurrency.go new file mode 100644 index 0000000..b2d222b --- /dev/null +++ b/concurrency.go @@ -0,0 +1,25 @@ +package main + +type WebsiteChecker func(string) bool +type result struct { + string + bool +} + +func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { + results := make(map[string]bool) + resultChannel := make(chan result) + + for _, url := range urls { + go func(u string) { + resultChannel <- result{u, wc(u)} + }(url) + } + + for i := 0; i < len(urls); i++ { + r := <-resultChannel + results[r.string] = r.bool + } + + return results +} diff --git a/concurrency_test.go b/concurrency_test.go new file mode 100644 index 0000000..f257341 --- /dev/null +++ b/concurrency_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "reflect" + "testing" + "time" +) + +func mockWebsiteChecker(url string) bool { + if url == "waat://furhurterwe.geds" { + return false + } + return true +} + +func slowStubWebsiteChecker(_ string) bool { + time.Sleep(20 * time.Millisecond) + return true +} + +func TestCheckWebsites(t *testing.T) { + websites := []string{ + "http://google.com", + "http://blog.gypsydave5.com", + "waat://furhurterwe.geds", + } + + want := map[string]bool{ + "http://google.com": true, + "http://blog.gypsydave5.com": true, + "waat://furhurterwe.geds": false, + } + + got := CheckWebsites(mockWebsiteChecker, websites) + + if !reflect.DeepEqual(want, got) { + t.Fatalf("Wanted %v, got %v", want, got) + } +} + +func BenchmarkCheckWebsites(b *testing.B) { + urls := make([]string, 100) + for i := 0; i < len(urls); i++ { + urls[i] = "a url" + } + + for i := 0; i < b.N; i++ { + CheckWebsites(slowStubWebsiteChecker, urls) + } +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..6ae2a0f --- /dev/null +++ b/context.go @@ -0,0 +1,22 @@ +package main + +import ( + "context" + "fmt" + "net/http" +) + +type Store interface { + Fetch(ctx context.Context) (string, error) +} + +func Server(store Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data, err := store.Fetch(r.Context()) + + if err != nil { + return // todo: log error however you like + } + fmt.Fprint(w, data) + } +} diff --git a/context_test.go b/context_test.go new file mode 100644 index 0000000..16b2d1e --- /dev/null +++ b/context_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +type SpyStore struct { + response string + t *testing.T +} + +func (s *SpyStore) Fetch(ctx context.Context) (string, error) { + data := make(chan string, 1) + + go func() { + var result string + for _, c := range s.response { + select { + case <-ctx.Done(): + s.t.Log("spy store got cancelled") + return + default: + time.Sleep(10 * time.Millisecond) + result += string(c) + } + } + data <- result + }() + + select { + case <-ctx.Done(): + return "", ctx.Err() + case res := <-data: + return res, nil + } +} + +type SpyResponseWriter struct { + written bool +} + +func (s *SpyResponseWriter) Header() http.Header { + s.written = true + return nil +} + +func (s *SpyResponseWriter) Write([]byte) (int, error) { + s.written = true + return 0, errors.New("not implemented") +} + +func (s *SpyResponseWriter) WriteHeader(statusCode int) { + s.written = true +} + +func TestServer(t *testing.T) { + data := "hello world" + t.Run("Returns data from store", func(t *testing.T) { + store := &SpyStore{response: data, t: t} + svr := Server(store) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + response := httptest.NewRecorder() + + svr.ServeHTTP(response, request) + + if response.Body.String() != data { + t.Errorf(`got "%s", want "%s"`, response.Body.String(), data) + } + }) + + t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) { + store := &SpyStore{response: data, t: t} + svr := Server(store) + + request := httptest.NewRequest(http.MethodGet, "/", nil) + + cancellingCtx, cancel := context.WithCancel(request.Context()) + time.AfterFunc(5*time.Millisecond, cancel) + request = request.WithContext(cancellingCtx) + + response := &SpyResponseWriter{} + + svr.ServeHTTP(response, request) + + if response.written { + t.Error("a response should not have been written") + } + }) +} diff --git a/di.go b/di.go new file mode 100644 index 0000000..debfa05 --- /dev/null +++ b/di.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "io" +) + +func Greet(writer io.Writer, name string) { + fmt.Fprintf(writer, "Hello, %s", name) +} + +//func main() { +// Greet(os.Stdout, "Elodie") +//} diff --git a/di_test.go b/di_test.go new file mode 100644 index 0000000..571148f --- /dev/null +++ b/di_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "bytes" + "testing" +) + +func TestGreet(t *testing.T) { + buffer := bytes.Buffer{} + Greet(&buffer, "Chris") + + got := buffer.String() + want := "Hello, Chris" + + if got != want { + t.Errorf("got %q want %q", got, want) + } +} diff --git a/dictionary.go b/dictionary.go new file mode 100644 index 0000000..8bb73cb --- /dev/null +++ b/dictionary.go @@ -0,0 +1,53 @@ +package main + +const ( + ErrNotFound = DictionaryErr("could not find the word you were looking for") + ErrWordExists = DictionaryErr("cannot add word because it already exists") + ErrWordDoesNotExist = DictionaryErr("cannot update word because it does not exist") +) + +type Dictionary map[string]string + +type DictionaryErr string + +func (e DictionaryErr) Error() string { + return string(e) +} + +func (d Dictionary) Search(word string) (string, error) { + definition, ok := d[word] + if !ok { + return "", ErrNotFound + } + return definition, nil +} + +func (d Dictionary) Add(word, definition string) error { + _, 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 { + _, err := d.Search(word) + switch err { + case ErrNotFound: + return ErrWordDoesNotExist + case nil: + d[word] = definition + default: + return err + } + return nil +} + +func (d Dictionary) Delete(word string) { + delete(d, word) +} diff --git a/dictionary_test.go b/dictionary_test.go new file mode 100644 index 0000000..6cfe746 --- /dev/null +++ b/dictionary_test.go @@ -0,0 +1,105 @@ +package main + +import "testing" + +func TestSearch(t *testing.T) { + dictionary := Dictionary{"test": "this is just a test"} + + t.Run("known word", func(t *testing.T) { + word := "test" + definition := "this is just a test" + + assertDefinition(t, dictionary, word, definition) + }) + + t.Run("unknown word", func(t *testing.T) { + _, got := dictionary.Search("unknown") + + assertError(t, got, ErrNotFound) + }) +} + +func TestAdd(t *testing.T) { + t.Run("new word", func(t *testing.T) { + dictionary := Dictionary{} + word := "test" + definition := "this is just a test" + + err := dictionary.Add(word, definition) + + assertError(t, err, nil) + assertDefinition(t, dictionary, word, definition) + }) + + t.Run("existing word", func(t *testing.T) { + word := "test" + definition := "this is just a test" + dictionary := Dictionary{word: definition} + err := dictionary.Add(word, "new test") + + assertError(t, err, ErrWordExists) + assertDefinition(t, dictionary, word, definition) + }) +} + +func TestUpdate(t *testing.T) { + t.Run("existing word", func(t *testing.T) { + word := "test" + definition := "this is just a test" + dictionary := Dictionary{word: definition} + newDefinition := "new definition" + + err := dictionary.Update(word, newDefinition) + + assertError(t, err, nil) + assertDefinition(t, dictionary, word, newDefinition) + }) + + t.Run("new word", func(t *testing.T) { + word := "test" + definition := "this is just a test" + dictionary := Dictionary{} + + err := dictionary.Update(word, definition) + + assertError(t, err, ErrWordDoesNotExist) + }) +} + +func TestDelete(t *testing.T) { + t.Run("existing word", func(t *testing.T) { + word := "test" + definition := "this is just a test" + dictionary := Dictionary{word: definition} + + dictionary.Delete(word) + + _, err := dictionary.Search(word) + if err != ErrNotFound { + t.Errorf("Expected %q to be deleted", word) + } + + }) +} + +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) + } + + if definition != got { + t.Errorf("got %q want %q", got, definition) + } +} + +func assertError(t testing.TB, got, want error) { + t.Helper() + + if got != want { + t.Errorf("got error %q want %q", got, want) + } +} diff --git a/fintech.go b/fintech.go new file mode 100644 index 0000000..ad184d5 --- /dev/null +++ b/fintech.go @@ -0,0 +1,38 @@ +package main + +import ( + "errors" + "fmt" +) + +type Bitcoin int + +type Stringer interface { + String() string +} + +type Wallet struct { + balance Bitcoin +} + +var ErrInsufficientFunds = errors.New("cannot withdraw, insufficient funds") + +func (w *Wallet) Deposit(amount Bitcoin) { + w.balance += amount +} + +func (w *Wallet) Withdraw(amount Bitcoin) error { + if amount > w.balance { + return ErrInsufficientFunds + } + w.balance -= amount + return nil +} + +func (w *Wallet) Balance() Bitcoin { + return w.balance +} + +func (b Bitcoin) String() string { + return fmt.Sprintf("%d BTC", b) +} diff --git a/fintech_test.go b/fintech_test.go new file mode 100644 index 0000000..b839159 --- /dev/null +++ b/fintech_test.go @@ -0,0 +1,53 @@ +package main + +import "testing" + +func TestWallet(t *testing.T) { + assertBalance := func(t testing.TB, wallet Wallet, want Bitcoin) { + t.Helper() + got := wallet.Balance() + + if got != want { + t.Errorf("got %s want %s", got, want) + } + } + + assertError := func(t testing.TB, got error, want error) { + t.Helper() + if got == nil { + t.Fatal("wanted an error but didn't get one") + } + + if got != want { + t.Errorf("got %q, want %q", got, want) + } + } + + assertNoError := func(t testing.TB, got error) { + t.Helper() + if got != nil { + t.Fatal("got an error but didn't want one") + } + } + + t.Run("Deposit", func(t *testing.T) { + wallet := Wallet{} + wallet.Deposit(Bitcoin(10)) + assertBalance(t, wallet, Bitcoin(10)) + }) + + t.Run("Withdraw", func(t *testing.T) { + wallet := Wallet{balance: Bitcoin(20)} + err := wallet.Withdraw(Bitcoin(10)) + assertNoError(t, err) + assertBalance(t, wallet, Bitcoin(10)) + }) + + t.Run("Withdraw error", func(t *testing.T) { + startingBalance := Bitcoin(20) + wallet := Wallet{balance: startingBalance} + err := wallet.Withdraw(Bitcoin(30)) + assertError(t, err, ErrInsufficientFunds) + assertBalance(t, wallet, startingBalance) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2e27131 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gotdd + +go 1.17 diff --git a/hello.go b/hello.go new file mode 100644 index 0000000..4a87ad0 --- /dev/null +++ b/hello.go @@ -0,0 +1,34 @@ +package main + +import "fmt" + +const spanish = "Spanish" +const french = "French" +const englishHelloPrefix = "Hello, " +const spanishHelloPrefix = "Hola, " +const frenchHelloPrefix = "Bonjour, " + +func main() { + fmt.Println(Hello("world", "")) +} + +func Hello(name string, language string) string { + if name == "" { + name = "World" + } + prefix := greetingPrefix(language) + + return prefix + name +} + +func greetingPrefix(language string) (prefix string) { + switch language { + case french: + prefix = frenchHelloPrefix + case spanish: + prefix = spanishHelloPrefix + default: + prefix = englishHelloPrefix + } + return +} diff --git a/hello_test.go b/hello_test.go new file mode 100644 index 0000000..c0caa2d --- /dev/null +++ b/hello_test.go @@ -0,0 +1,35 @@ +package main + +import "testing" + +func TestHello(t *testing.T) { + assertCorrectMessage := func(t testing.TB, got, want string) { + t.Helper() + if got != want { + t.Errorf("got %q want %q", got, want) + } + } + t.Run("saying hello to people", func(t *testing.T) { + got := Hello("Chris", "") + want := "Hello, Chris" + assertCorrectMessage(t, got, want) + }) + + t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) { + got := Hello("", "") + want := "Hello, World" + assertCorrectMessage(t, got, want) + }) + + t.Run("In spanish", func(t *testing.T) { + got := Hello("Elodie", "Spanish") + want := "Hola, Elodie" + assertCorrectMessage(t, got, want) + }) + + t.Run("In French", func(t *testing.T) { + got := Hello("Frankie", "French") + want := "Bonjour, Frankie" + assertCorrectMessage(t, got, want) + }) +} diff --git a/mocking.go b/mocking.go new file mode 100644 index 0000000..98f1495 --- /dev/null +++ b/mocking.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "io" + "os" + "time" +) + +const finalWord = "Go!" +const countdownStart = 3 + +type Sleeper interface { + Sleep() +} + +type DefaultSleeper struct { +} + +type ConfigurableSleeper struct { + duration time.Duration + sleep func(duration time.Duration) +} + +func Countdown_main() { + sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep} + Countdown(os.Stdout, sleeper) +} + +func (d *DefaultSleeper) Sleep() { + time.Sleep(1 * time.Second) +} + +func Countdown(out io.Writer, sleeper Sleeper) { + for i := countdownStart; i > 0; i-- { + sleeper.Sleep() + fmt.Fprintln(out, i) + } + sleeper.Sleep() + fmt.Fprint(out, finalWord) +} + +func (c *ConfigurableSleeper) Sleep() { + c.sleep(c.duration) +} diff --git a/mocking_test.go b/mocking_test.go new file mode 100644 index 0000000..2bf60e0 --- /dev/null +++ b/mocking_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "bytes" + "reflect" + "testing" + "time" +) + +type SpySleeper struct { + Calls int +} + +func (s *SpySleeper) Sleep() { + s.Calls++ +} + +type SpyTime struct { + durationSlept time.Duration +} + +func (s *SpyTime) Sleep(duration time.Duration) { + s.durationSlept = duration +} + +type SpyCountdownOperations struct { + Calls []string +} + +func (s *SpyCountdownOperations) Sleep() { + s.Calls = append(s.Calls, sleep) +} + +func (s *SpyCountdownOperations) Write(p []byte) (n int, err error) { + s.Calls = append(s.Calls, write) + return +} + +const write = "write" +const sleep = "sleep" + +func TestCountdown(t *testing.T) { + t.Run("default countdown test", func(t *testing.T) { + buffer := &bytes.Buffer{} + spySleeper := &SpySleeper{} + + Countdown(buffer, spySleeper) + + got := buffer.String() + want := `3 +2 +1 +Go!` + + if got != want { + t.Errorf("got %q want %q", got, want) + } + + if spySleeper.Calls != 4 { + t.Errorf("not enough calls to sleeper, want 4 got %d", spySleeper.Calls) + } + }) + + t.Run("sleep before every print", func(t *testing.T) { + spySleepPrinter := &SpyCountdownOperations{} + Countdown(spySleepPrinter, spySleepPrinter) + + want := []string{ + sleep, + write, + sleep, + write, + sleep, + write, + sleep, + write, + } + + if !reflect.DeepEqual(want, spySleepPrinter.Calls) { + t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls) + } + }) +} + +func TestConfigurableSleeper(t *testing.T) { + sleepTime := 5 * time.Second + + spyTime := &SpyTime{} + sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep} + sleeper.Sleep() + + if spyTime.durationSlept != sleepTime { + t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept) + } +} diff --git a/reflection.go b/reflection.go new file mode 100644 index 0000000..4c043ec --- /dev/null +++ b/reflection.go @@ -0,0 +1,57 @@ +package main + +import "reflect" + +type Person struct { + Name string + Profile Profile +} + +type Profile struct { + Age int + City string +} + +func walk(x interface{}, fn func(input string)) { + val := getValue(x) + + walkValue := func(value reflect.Value) { + walk(value.Interface(), fn) + } + + switch val.Kind() { + case reflect.String: + fn(val.String()) + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + walkValue(val.Field(i)) + } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + walkValue(val.Index(i)) + } + case reflect.Map: + for _, key := range val.MapKeys() { + walkValue(val.MapIndex(key)) + } + case reflect.Chan: + for v, ok := val.Recv(); ok; v, ok = val.Recv() { + walkValue(v) + } + case reflect.Func: + valFnResult := val.Call(nil) + for _, res := range valFnResult { + walkValue(res) + } + } +} + +func getValue(x interface{}) reflect.Value { + val := reflect.ValueOf(x) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + return val +} diff --git a/reflection_test.go b/reflection_test.go new file mode 100644 index 0000000..6ba77ae --- /dev/null +++ b/reflection_test.go @@ -0,0 +1,148 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestWalk(t *testing.T) { + cases := []struct { + Name string + Input interface{} + ExpectedCalls []string + }{ + { + "Struct with one string field", + struct { + Name string + }{"Chris"}, + []string{"Chris"}, + }, + { + "Struct with two string field", + struct { + Name string + City string + }{"Chris", "London"}, + []string{"Chris", "London"}, + }, + { + "Struct with non string fields", + struct { + Name string + City int + }{"Chris", 33}, + []string{"Chris"}, + }, + { + "Nested fields", + Person{ + "Chris", + Profile{33, "London"}, + }, + []string{"Chris", "London"}, + }, + { + "Pointers to things", + &Person{ + "Chris", + Profile{33, "London"}, + }, + []string{"Chris", "London"}, + }, + { + "Slices", + []Profile{ + {33, "London"}, + {34, "Reykjavik"}, + }, + []string{"London", "Reykjavik"}, + }, + { + "Arrays", + [2]Profile{ + {33, "London"}, + {34, "Reykjavik"}, + }, + []string{"London", "Reykjavik"}, + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + var got []string + walk(test.Input, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, test.ExpectedCalls) { + t.Errorf("got %v, want %v", got, test.ExpectedCalls) + } + }) + } + + t.Run("with maps", func(t *testing.T) { + aMap := map[string]string{ + "Foo": "Bar", + "Baz": "Boz", + } + var got []string + walk(aMap, func(input string) { + got = append(got, input) + }) + + assertContains(t, got, "Bar") + assertContains(t, got, "Boz") + }) + + t.Run("with channels", func(t *testing.T) { + aChannel := make(chan Profile) + + go func() { + aChannel <- Profile{33, "Berlin"} + aChannel <- Profile{34, "Katowice"} + close(aChannel) + }() + + var got []string + want := []string{"Berlin", "Katowice"} + + walk(aChannel, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } + }) + + t.Run("with function", func(t *testing.T) { + aFunction := func() (Profile, Profile) { + return Profile{33, "Berlin"}, Profile{34, "Katowice"} + } + + var got []string + want := []string{"Berlin", "Katowice"} + + walk(aFunction, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } + }) +} + +func assertContains(t testing.TB, haystack []string, needle string) { + t.Helper() + contains := false + for _, x := range haystack { + if x == needle { + contains = true + } + } + if !contains { + t.Errorf("expected %+v to contain %q but it didn't", haystack, needle) + } +} diff --git a/repeat.go b/repeat.go new file mode 100644 index 0000000..3cf751e --- /dev/null +++ b/repeat.go @@ -0,0 +1,9 @@ +package main + +func Repeat(character string) string { + var repeated string + for i := 0; i < 5; i++ { + repeated += character + } + return repeated +} diff --git a/repeat_test.go b/repeat_test.go new file mode 100644 index 0000000..a8795a1 --- /dev/null +++ b/repeat_test.go @@ -0,0 +1,18 @@ +package main + +import "testing" + +func TestRepeat(t *testing.T) { + repeated := Repeat("a") + expected := "aaaaa" + + if repeated != expected { + t.Errorf("Expected %q but got %q", expected, repeated) + } +} + +func BenchmarkRepeat(b *testing.B) { + for i := 0; i < b.N; i++ { + Repeat("a") + } +} diff --git a/roman_numerals.go b/roman_numerals.go new file mode 100644 index 0000000..6716f62 --- /dev/null +++ b/roman_numerals.go @@ -0,0 +1,90 @@ +package main + +import "strings" + +// ConvertToArabic converts a Roman Numeral to an Arabic number. +func ConvertToArabic(roman string) (total uint16) { + for _, symbols := range windowedRoman(roman).Symbols() { + total += allRomanNumerals.ValueOf(symbols...) + } + return +} + +// ConvertToRoman converts an Arabic number to a Roman Numeral. +func ConvertToRoman(arabic uint16) string { + var result strings.Builder + + for _, numeral := range allRomanNumerals { + for arabic >= numeral.Value { + result.WriteString(numeral.Symbol) + arabic -= numeral.Value + } + } + + return result.String() +} + +type romanNumeral struct { + Value uint16 + Symbol string +} + +type romanNumerals []romanNumeral + +func (r romanNumerals) ValueOf(symbols ...byte) uint16 { + symbol := string(symbols) + for _, s := range r { + if s.Symbol == symbol { + return s.Value + } + } + + return 0 +} + +func (r romanNumerals) Exists(symbols ...byte) bool { + symbol := string(symbols) + for _, s := range r { + if s.Symbol == symbol { + return true + } + } + return false +} + +var allRomanNumerals = romanNumerals{ + {1000, "M"}, + {900, "CM"}, + {500, "D"}, + {400, "CD"}, + {100, "C"}, + {90, "XC"}, + {50, "L"}, + {40, "XL"}, + {10, "X"}, + {9, "IX"}, + {5, "V"}, + {4, "IV"}, + {1, "I"}, +} + +type windowedRoman string + +func (w windowedRoman) Symbols() (symbols [][]byte) { + for i := 0; i < len(w); i++ { + symbol := w[i] + notAtEnd := i+1 < len(w) + + if notAtEnd && isSubtractive(symbol) && allRomanNumerals.Exists(symbol, w[i+1]) { + symbols = append(symbols, []byte{symbol, w[i+1]}) + i++ + } else { + symbols = append(symbols, []byte{symbol}) + } + } + return +} + +func isSubtractive(symbol uint8) bool { + return symbol == 'I' || symbol == 'X' || symbol == 'C' +} diff --git a/roman_numerals_test.go b/roman_numerals_test.go new file mode 100644 index 0000000..1d43332 --- /dev/null +++ b/roman_numerals_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "testing" + "testing/quick" +) + +var ( + cases = []struct { + Arabic uint16 + Roman string + }{ + {Arabic: 1, Roman: "I"}, + {Arabic: 2, Roman: "II"}, + {Arabic: 3, Roman: "III"}, + {Arabic: 4, Roman: "IV"}, + {Arabic: 5, Roman: "V"}, + {Arabic: 6, Roman: "VI"}, + {Arabic: 7, Roman: "VII"}, + {Arabic: 8, Roman: "VIII"}, + {Arabic: 9, Roman: "IX"}, + {Arabic: 10, Roman: "X"}, + {Arabic: 14, Roman: "XIV"}, + {Arabic: 18, Roman: "XVIII"}, + {Arabic: 20, Roman: "XX"}, + {Arabic: 39, Roman: "XXXIX"}, + {Arabic: 40, Roman: "XL"}, + {Arabic: 47, Roman: "XLVII"}, + {Arabic: 49, Roman: "XLIX"}, + {Arabic: 50, Roman: "L"}, + {Arabic: 100, Roman: "C"}, + {Arabic: 90, Roman: "XC"}, + {Arabic: 400, Roman: "CD"}, + {Arabic: 500, Roman: "D"}, + {Arabic: 900, Roman: "CM"}, + {Arabic: 1000, Roman: "M"}, + {Arabic: 1984, Roman: "MCMLXXXIV"}, + {Arabic: 3999, Roman: "MMMCMXCIX"}, + {Arabic: 2014, Roman: "MMXIV"}, + {Arabic: 1006, Roman: "MVI"}, + {Arabic: 798, Roman: "DCCXCVIII"}, + } +) + +func TestConvertingToRomanNumerals(t *testing.T) { + for _, test := range cases { + t.Run(fmt.Sprintf("%d gets converted to '%s", test.Arabic, test.Roman), func(t *testing.T) { + got := ConvertToRoman(test.Arabic) + if got != test.Roman { + t.Errorf("got %q, want %q", got, test.Roman) + } + }) + } +} + +func TestConvertingToArabic(t *testing.T) { + for _, test := range cases { + t.Run(fmt.Sprintf("%q gets converted to %d", test.Roman, test.Arabic), func(t *testing.T) { + got := ConvertToArabic(test.Roman) + if got != test.Arabic { + t.Errorf("got %d, want %d", got, test.Arabic) + } + }) + } +} + +func TestPropertiesOfConversion(t *testing.T) { + assertion := func(arabic uint16) bool { + if arabic > 3999 { + return true + } + t.Log("testing", arabic) + roman := ConvertToRoman(arabic) + fromRoman := ConvertToArabic(roman) + return fromRoman == arabic + } + + if err := quick.Check(assertion, &quick.Config{ + MaxCount: 1000, + }); err != nil { + t.Error("failed checks", err) + } +} diff --git a/select.go b/select.go new file mode 100644 index 0000000..08be93d --- /dev/null +++ b/select.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "net/http" + "time" +) + +var tenSecondTimeout = 10 * time.Second + +func Racer(a, b string) (winner string, err error) { + return ConfigurableRacer(a, b, tenSecondTimeout) +} + +func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, err error) { + select { + case <-ping(a): + return a, nil + case <-ping(b): + return b, nil + case <-time.After(timeout): + return "", fmt.Errorf("timed out waiting for %s and %s", a, b) + } +} + +func ping(url string) chan struct{} { + ch := make(chan struct{}) + go func() { + http.Get(url) + close(ch) + }() + return ch +} + +func measureResponseTime(url string) time.Duration { + start := time.Now() + http.Get(url) + return time.Since(start) +} diff --git a/select_test.go b/select_test.go new file mode 100644 index 0000000..875a659 --- /dev/null +++ b/select_test.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestRacer(t *testing.T) { + t.Run("Test that fast server returns", func(t *testing.T) { + slowServer := makeDelayedServer(time.Millisecond * 20) + fastServer := makeDelayedServer(time.Millisecond * 0) + + defer slowServer.Close() + defer fastServer.Close() + + slowURL := slowServer.URL + fastURL := fastServer.URL + + want := fastURL + got, _ := Racer(slowURL, fastURL) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } + }) + + t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) { + server := makeDelayedServer(25 * time.Millisecond) + + defer server.Close() + + _, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond) + + if err == nil { + t.Error("expected an error, but didn't get one") + } + }) +} + +func makeDelayedServer(delay time.Duration) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(delay) + w.WriteHeader(http.StatusOK) + })) +} diff --git a/shapes.go b/shapes.go new file mode 100644 index 0000000..af47441 --- /dev/null +++ b/shapes.go @@ -0,0 +1,37 @@ +package main + +import "math" + +type Shape interface { + Area() float64 +} + +type Rectangle struct { + Width float64 + Height float64 +} + +type Circle struct { + Radius float64 +} + +type Triangle struct { + Base float64 + Height float64 +} + +func Perimeter(rectangle Rectangle) float64 { + return 2 * (rectangle.Width + rectangle.Height) +} + +func (r Rectangle) Area() float64 { + return r.Height * r.Width +} + +func (c Circle) Area() float64 { + return math.Pi * c.Radius * c.Radius +} + +func (t Triangle) Area() float64 { + return 0.5 * t.Base * t.Height +} diff --git a/shapes_test.go b/shapes_test.go new file mode 100644 index 0000000..7a24920 --- /dev/null +++ b/shapes_test.go @@ -0,0 +1,33 @@ +package main + +import "testing" + +func TestPerimeter(t *testing.T) { + rectangle := Rectangle{10.0, 10.0} + got := Perimeter(rectangle) + want := 40.0 + + if got != want { + t.Errorf("got %.2f want %.2f", got, want) + } +} + +func TestArea(t *testing.T) { + + areaTests := []struct { + name string + shape Shape + hasArea float64 + }{ + {name: "Rectangle", shape: Rectangle{12, 6}, hasArea: 72.0}, + {name: "Circle", shape: Circle{10}, hasArea: 314.1592653589793}, + {name: "Triangle", shape: Triangle{12, 6}, hasArea: 36}, + } + + for _, tt := range areaTests { + got := tt.shape.Area() + if got != tt.hasArea { + t.Errorf("got %g want %g", got, tt.hasArea) + } + } +} diff --git a/sync.go b/sync.go new file mode 100644 index 0000000..43b72f7 --- /dev/null +++ b/sync.go @@ -0,0 +1,22 @@ +package main + +import "sync" + +type Counter struct { + mu sync.Mutex + value int +} + +func (c *Counter) Inc() { + c.mu.Lock() + defer c.mu.Unlock() + c.value++ +} + +func (c *Counter) Value() int { + return c.value +} + +func NewCounter() *Counter { + return &Counter{} +} diff --git a/sync_test.go b/sync_test.go new file mode 100644 index 0000000..1686b46 --- /dev/null +++ b/sync_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "sync" + "testing" +) + +func TestCounter(t *testing.T) { + t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { + counter := NewCounter() + counter.Inc() + counter.Inc() + counter.Inc() + + assertCounter(t, counter, 3) + }) + + t.Run("it runs safely concurrently", func(t *testing.T) { + wantedCount := 1000 + counter := NewCounter() + + var wg sync.WaitGroup + wg.Add(wantedCount) + + for i := 0; i < wantedCount; i++ { + go func() { + counter.Inc() + wg.Done() + }() + } + wg.Wait() + + assertCounter(t, counter, wantedCount) + }) +} + +func assertCounter(t testing.TB, got *Counter, want int) { + t.Helper() + if got.Value() != want { + t.Errorf("got %d, want %d", got.Value(), want) + } +}