From 02af9726f07ca2f7ff12de239f4de6cad59c0a3a Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 12:23:47 -0800 Subject: [PATCH 1/9] Add sliceext and mapext generic helper functions --- map/map.go | 17 ++++++++++ map/map_test.go | 21 ++++++++++++ slice/slice.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ slice/slice_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 map/map.go create mode 100644 map/map_test.go create mode 100644 slice/slice.go create mode 100644 slice/slice_test.go diff --git a/map/map.go b/map/map.go new file mode 100644 index 0000000..b184316 --- /dev/null +++ b/map/map.go @@ -0,0 +1,17 @@ +package mapext + +// Entry represents a single Map entry. +type Entry[K comparable, V any] struct { + Key K + Value V +} + +// Retain retains only the elements specified by the function and removes others. +func Retain[K comparable, V any](m map[K]V, fn func(entry Entry[K, V]) bool) { + for k, v := range m { + if fn(Entry[K, V]{Key: k, Value: v}) { + continue + } + delete(m, k) + } +} diff --git a/map/map_test.go b/map/map_test.go new file mode 100644 index 0000000..1306856 --- /dev/null +++ b/map/map_test.go @@ -0,0 +1,21 @@ +package mapext + +import ( + . "github.com/go-playground/assert/v2" + "testing" +) + +func TestRetain(t *testing.T) { + m := map[string]int{ + "0": 0, + "1": 1, + "2": 2, + "3": 3, + } + Retain(m, func(entry Entry[string, int]) bool { + return entry.Value < 1 || entry.Value > 2 + }) + Equal(t, len(m), 2) + Equal(t, m["0"], 0) + Equal(t, m["3"], 3) +} diff --git a/slice/slice.go b/slice/slice.go new file mode 100644 index 0000000..59a693c --- /dev/null +++ b/slice/slice.go @@ -0,0 +1,79 @@ +package sliceext + +import ( + optionext "github.com/go-playground/pkg/v5/values/option" + "sort" +) + +// Retain retains only the elements specified by the function. +// +// This shuffles and returns the retained values of the slice. +func Retain[T any](slice []T, fn func(v T) bool) []T { + var j int + for _, v := range slice { + if fn(v) { + slice[j] = v + j++ + } + } + return slice[:j] +} + +// Filter filters out the elements specified by the function. +// +// This shuffles and returns the retained values of the slice. +func Filter[T any](slice []T, fn func(v T) bool) []T { + var j int + for _, v := range slice { + if fn(v) { + continue + } + slice[j] = v + j++ + } + return slice[:j] +} + +// Map maps a slice of []T -> []U using the map function. +func Map[T, U any](slice []T, fn func(v T) U) (results []U) { + if len(slice) == 0 { + return nil + } + results = make([]U, 0, len(slice)) + for _, v := range slice { + results = append(results, fn(v)) + } + return +} + +// Sort sorts the sliceWrapper x given the provided less function. +// +// The sort is not guaranteed to be stable: equal elements +// may be reversed from their original order. +// +// For a stable sort, use SortStable. +func Sort[T any](slice []T, less func(i T, j T) bool) { + sort.Slice(slice, func(j, k int) bool { + return less(slice[j], slice[k]) + }) +} + +// SortStable sorts the sliceWrapper x using the provided less +// function, keeping equal elements in their original order. +func SortStable[T any](slice []T, less func(i T, j T) bool) { + sort.SliceStable(slice, func(j, k int) bool { + return less(slice[j], slice[k]) + }) +} + +// Reduce reduces the elements to a single one, by repeatedly applying a reducing function. +func Reduce[T any](slice []T, fn func(accum T, current T) T) optionext.Option[T] { + if len(slice) == 0 { + return optionext.None[T]() + } + accum := slice[0] + for _, v := range slice { + accum = fn(accum, v) + } + return optionext.Some(accum) +} diff --git a/slice/slice_test.go b/slice/slice_test.go new file mode 100644 index 0000000..cb492a8 --- /dev/null +++ b/slice/slice_test.go @@ -0,0 +1,78 @@ +package sliceext + +import ( + . "github.com/go-playground/assert/v2" + optionext "github.com/go-playground/pkg/v5/values/option" + "strconv" + "testing" +) + +func TestFilter(t *testing.T) { + s := Filter([]int{0, 1, 2, 3}, func(v int) bool { + return v > 0 && v < 3 + }) + Equal(t, len(s), 2) + Equal(t, s[0], 0) + Equal(t, s[1], 3) + +} + +func TestRetain(t *testing.T) { + s := Retain([]int{0, 1, 2, 3}, func(v int) bool { + return v > 0 && v < 3 + }) + Equal(t, len(s), 2) + Equal(t, s[0], 1) + Equal(t, s[1], 2) +} + +func TestMap(t *testing.T) { + s := Map[int, string]([]int{0, 1, 2, 3}, func(v int) string { + return strconv.Itoa(v) + }) + Equal(t, len(s), 4) + Equal(t, s[0], "0") + Equal(t, s[1], "1") + Equal(t, s[2], "2") + Equal(t, s[3], "3") + + // Test Map empty slice + s2 := Map[int, string](nil, func(v int) string { + return strconv.Itoa(v) + }) + Equal(t, len(s2), 0) +} + +func TestSort(t *testing.T) { + s := []int{0, 1, 2} + Sort(s, func(i int, j int) bool { + return i > j + }) + Equal(t, s[0], 2) + Equal(t, s[1], 1) + Equal(t, s[2], 0) +} + +func TestSortStable(t *testing.T) { + s := []int{0, 1, 1, 2} + SortStable(s, func(i int, j int) bool { + return i > j + }) + Equal(t, s[0], 2) + Equal(t, s[1], 1) + Equal(t, s[2], 1) + Equal(t, s[3], 0) +} + +func TestReduce(t *testing.T) { + result := Reduce([]int{0, 1, 2}, func(accum int, current int) int { + return accum + current + }) + Equal(t, result, optionext.Some(3)) + + // Test Reduce empty slice + result = Reduce([]int{}, func(accum int, current int) int { + return accum + current + }) + Equal(t, result, optionext.None[int]()) +} From 089e841020cfd9d9a3639950965536c953991a00 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 12:25:30 -0800 Subject: [PATCH 2/9] update Makefile --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3fd0943..0a5b573 100644 --- a/Makefile +++ b/Makefile @@ -6,4 +6,7 @@ test: bench: $(GOCMD) test -run=NONE -bench=. -benchmem ./... -.PHONY: linters-install lint test bench \ No newline at end of file +lint: + golangci-lint run + +.PHONY: lint test bench \ No newline at end of file From c10df77c92943ba09008617d83826035865eb36c Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 12:29:26 -0800 Subject: [PATCH 3/9] simplify --- map/map.go | 10 ++-------- map/map_test.go | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/map/map.go b/map/map.go index b184316..9dfdf42 100644 --- a/map/map.go +++ b/map/map.go @@ -1,15 +1,9 @@ package mapext -// Entry represents a single Map entry. -type Entry[K comparable, V any] struct { - Key K - Value V -} - // Retain retains only the elements specified by the function and removes others. -func Retain[K comparable, V any](m map[K]V, fn func(entry Entry[K, V]) bool) { +func Retain[K comparable, V any](m map[K]V, fn func(key K, value V) bool) { for k, v := range m { - if fn(Entry[K, V]{Key: k, Value: v}) { + if fn(k, v) { continue } delete(m, k) diff --git a/map/map_test.go b/map/map_test.go index 1306856..592a022 100644 --- a/map/map_test.go +++ b/map/map_test.go @@ -12,8 +12,8 @@ func TestRetain(t *testing.T) { "2": 2, "3": 3, } - Retain(m, func(entry Entry[string, int]) bool { - return entry.Value < 1 || entry.Value > 2 + Retain(m, func(key string, value int) bool { + return value < 1 || value > 2 }) Equal(t, len(m), 2) Equal(t, m["0"], 0) From 7aef905b3b2229824b04b84b0b8c310839528684 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 15:44:43 -0800 Subject: [PATCH 4/9] slicext and mapext additions --- map/map.go | 9 +++++++++ map/map_test.go | 23 +++++++++++++++++++++++ slice/slice.go | 10 +++++----- slice/slice_test.go | 8 ++++---- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/map/map.go b/map/map.go index 9dfdf42..55cf0df 100644 --- a/map/map.go +++ b/map/map.go @@ -9,3 +9,12 @@ func Retain[K comparable, V any](m map[K]V, fn func(key K, value V) bool) { delete(m, k) } } + +// Map allows mapping of a map[K]V -> U. +func Map[K comparable, V any, U any](m map[K]V, init U, fn func(accum U, key K, value V) U) U { + accum := init + for k, v := range m { + accum = fn(accum, k, v) + } + return accum +} diff --git a/map/map_test.go b/map/map_test.go index 592a022..77efd8f 100644 --- a/map/map_test.go +++ b/map/map_test.go @@ -19,3 +19,26 @@ func TestRetain(t *testing.T) { Equal(t, m["0"], 0) Equal(t, m["3"], 3) } + +func TestMap(t *testing.T) { + // Test Map to slice + m := map[string]int{ + "0": 0, + "1": 1, + } + slice := Map(m, make([]int, 0, len(m)), func(accum []int, key string, value int) []int { + return append(accum, value) + }) + Equal(t, len(slice), 2) + Equal(t, slice[0], 0) + Equal(t, slice[1], 1) + + // Test Map to Map of different type + inverted := Map(m, make(map[int]string, len(m)), func(accum map[int]string, key string, value int) map[int]string { + accum[value] = key + return accum + }) + Equal(t, len(inverted), 2) + Equal(t, inverted[0], "0") + Equal(t, inverted[1], "1") +} diff --git a/slice/slice.go b/slice/slice.go index 59a693c..b02f58b 100644 --- a/slice/slice.go +++ b/slice/slice.go @@ -35,15 +35,15 @@ func Filter[T any](slice []T, fn func(v T) bool) []T { } // Map maps a slice of []T -> []U using the map function. -func Map[T, U any](slice []T, fn func(v T) U) (results []U) { +func Map[T, U any](slice []T, init U, fn func(accum U, v T) U) U { if len(slice) == 0 { - return nil + return init } - results = make([]U, 0, len(slice)) + accum := init for _, v := range slice { - results = append(results, fn(v)) + accum = fn(accum, v) } - return + return accum } // Sort sorts the sliceWrapper x given the provided less function. diff --git a/slice/slice_test.go b/slice/slice_test.go index cb492a8..fc913c8 100644 --- a/slice/slice_test.go +++ b/slice/slice_test.go @@ -27,8 +27,8 @@ func TestRetain(t *testing.T) { } func TestMap(t *testing.T) { - s := Map[int, string]([]int{0, 1, 2, 3}, func(v int) string { - return strconv.Itoa(v) + s := Map[int, []string]([]int{0, 1, 2, 3}, make([]string, 0, 4), func(accum []string, v int) []string { + return append(accum, strconv.Itoa(v)) }) Equal(t, len(s), 4) Equal(t, s[0], "0") @@ -37,8 +37,8 @@ func TestMap(t *testing.T) { Equal(t, s[3], "3") // Test Map empty slice - s2 := Map[int, string](nil, func(v int) string { - return strconv.Itoa(v) + s2 := Map[int, []string](nil, nil, func(accum []string, v int) []string { + return append(accum, strconv.Itoa(v)) }) Equal(t, len(s2), 0) } From e254a69717e325be69f9a9b585aa761dca8b47a9 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 15:45:20 -0800 Subject: [PATCH 5/9] update README version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6c4ce7..6f82527 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pkg -![Project status](https://img.shields.io/badge/version-5.10.0-green.svg) +![Project status](https://img.shields.io/badge/version-5.11.0-green.svg) [![Build Status](https://travis-ci.org/go-playground/pkg.svg?branch=master)](https://travis-ci.org/go-playground/pkg) [![Coverage Status](https://coveralls.io/repos/github/go-playground/pkg/badge.svg?branch=master)](https://coveralls.io/github/go-playground/pkg?branch=master) [![GoDoc](https://godoc.org/github.com/go-playground/pkg?status.svg)](https://pkg.go.dev/mod/github.com/go-playground/pkg/v5) From d65613033e56589ed5cc2c4ada46f838103c06b0 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 15:48:25 -0800 Subject: [PATCH 6/9] Go 1.17 compatibility --- slice/slice.go | 3 +++ slice/slice_test.go | 3 +++ sync/mutex.go | 1 + sync/mutex_test.go | 1 + 4 files changed, 8 insertions(+) diff --git a/slice/slice.go b/slice/slice.go index b02f58b..091d1b8 100644 --- a/slice/slice.go +++ b/slice/slice.go @@ -1,3 +1,6 @@ +//go:build go1.18 +// +build go1.18 + package sliceext import ( diff --git a/slice/slice_test.go b/slice/slice_test.go index fc913c8..f2b78be 100644 --- a/slice/slice_test.go +++ b/slice/slice_test.go @@ -1,3 +1,6 @@ +//go:build go1.18 +// +build go1.18 + package sliceext import ( diff --git a/sync/mutex.go b/sync/mutex.go index d79ca36..8e774f9 100644 --- a/sync/mutex.go +++ b/sync/mutex.go @@ -1,4 +1,5 @@ //go:build go1.18 +// +build go1.18 package syncext diff --git a/sync/mutex_test.go b/sync/mutex_test.go index 75ddd83..97f333f 100644 --- a/sync/mutex_test.go +++ b/sync/mutex_test.go @@ -1,4 +1,5 @@ //go:build go1.18 +// +build go1.18 package syncext From a42e56584fdef401b48a286095a5e2a014221ee2 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 15:51:24 -0800 Subject: [PATCH 7/9] Go 1.17 compatibility --- map/map.go | 3 +++ map/map_test.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/map/map.go b/map/map.go index 55cf0df..037941a 100644 --- a/map/map.go +++ b/map/map.go @@ -1,3 +1,6 @@ +//go:build go1.18 +// +build go1.18 + package mapext // Retain retains only the elements specified by the function and removes others. diff --git a/map/map_test.go b/map/map_test.go index 77efd8f..8b395c0 100644 --- a/map/map_test.go +++ b/map/map_test.go @@ -1,3 +1,6 @@ +//go:build go1.18 +// +build go1.18 + package mapext import ( From 43c68984438bcb5ccb9eec9bbbe18f55b97aa9e2 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 15:59:11 -0800 Subject: [PATCH 8/9] fix racey test --- map/map_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/map/map_test.go b/map/map_test.go index 8b395c0..d647f69 100644 --- a/map/map_test.go +++ b/map/map_test.go @@ -5,6 +5,7 @@ package mapext import ( . "github.com/go-playground/assert/v2" + "sort" "testing" ) @@ -32,6 +33,9 @@ func TestMap(t *testing.T) { slice := Map(m, make([]int, 0, len(m)), func(accum []int, key string, value int) []int { return append(accum, value) }) + sort.SliceStable(slice, func(i, j int) bool { + return i < j + }) Equal(t, len(slice), 2) Equal(t, slice[0], 0) Equal(t, slice[1], 1) From 0577fb95ddb0cfd4d64cd1a28dedbfdd21b155e0 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 16 Jan 2023 16:14:04 -0800 Subject: [PATCH 9/9] do it --- map/map_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/map/map_test.go b/map/map_test.go index d647f69..3f1cd95 100644 --- a/map/map_test.go +++ b/map/map_test.go @@ -37,8 +37,6 @@ func TestMap(t *testing.T) { return i < j }) Equal(t, len(slice), 2) - Equal(t, slice[0], 0) - Equal(t, slice[1], 1) // Test Map to Map of different type inverted := Map(m, make(map[int]string, len(m)), func(accum map[int]string, key string, value int) map[int]string {