├── .github └── workflows │ └── go.yaml ├── .gitignore ├── LICENSE ├── README.md ├── ch ├── ch.go └── ch_test.go ├── docs ├── .nojekyll ├── README.md ├── _sidebar.md ├── channel.md ├── index.html ├── map.md └── slice.md ├── go.mod ├── go.sum ├── kv ├── kv.go └── kv_test.go └── slice ├── slice.go └── slice_test.go /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: run tests 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | stable: false 20 | go-version: 1.18.0-beta1 21 | 22 | - name: Build 23 | run: go build -v ./... 24 | 25 | coverage: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Install Go 29 | if: success() 30 | uses: actions/setup-go@v2 31 | with: 32 | stable: false 33 | go-version: 1.18.0-beta1 34 | - name: Checkout code 35 | uses: actions/checkout@v2 36 | - name: Calc coverage 37 | run: | 38 | go test -v -covermode=count -coverprofile=coverage.out ./... 39 | - name: Convert coverage.out to coverage.lcov 40 | uses: jandelgado/gcov2lcov-action@v1.0.6 41 | - name: Coveralls 42 | uses: coverallsapp/github-action@v1.1.2 43 | with: 44 | github-token: ${{ secrets.github_token }} 45 | path-to-lcov: coverage.lcov -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nikita Galushko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fx 2 | [![Status](https://github.com/nikgalushko/fx/actions/workflows/go.yaml/badge.svg)](https://github.com/nikgalushko/fx/actions/workflows/go.yaml) [![Coverage Status](https://coveralls.io/repos/github/nikgalushko/fx/badge.svg?branch=main)](https://coveralls.io/github/nikgalushko/fx?branch=main) 3 | 4 | 5 | Fx is a useful functional programming helpers without using `interface{}` or reflect. 6 | 7 | Support **only** Go 1.18+. 8 | 9 | ## Features 10 | - Slice 11 | - Each 12 | - Collect 13 | - Reduce 14 | - Find 15 | - Filter 16 | - Every 17 | - Some 18 | - Contains 19 | - Max 20 | - Min 21 | - GroupBy 22 | - Sample 23 | - SampleN 24 | - Union 25 | - Intersection 26 | - Uniq 27 | - IndexOf 28 | - LastIndexOf 29 | - Reverse 30 | - Map 31 | - Keys 32 | - Values 33 | - Each 34 | - Filter 35 | - Channel 36 | - Merge 37 | 38 | ## Documentation 39 | 40 | Documentation with examples can be found here: https://nikgalushko.github.io/fx/ 41 | 42 | ## Installation 43 | 44 | slice helpers `go get github.com/nikgalushko/fx/slice` 45 | 46 | map helpers `go get github.com/nikgalushko/fx/kv` 47 | 48 | channel helpers `go get github.com/nikgalushko/fx/ch` 49 | 50 | ## Usage 51 | 52 | ```go 53 | import ( 54 | "fmt" 55 | 56 | "github.com/nikgalushko/fx/kv" 57 | "github.com/nikgalushko/fx/slice" 58 | ) 59 | 60 | type ( 61 | ID string 62 | 63 | Attribute struct { 64 | Value string 65 | } 66 | ) 67 | 68 | func main() { 69 | m := map[ID]Attribute{ 70 | ID("1"): {Value: "blah"}, 71 | ID("1861"): {Value: "!"}, 72 | ID("1234"): {Value: "yeah"}, 73 | } 74 | 75 | fmt.Println("ids", kv.Keys(m), "contains special", slice.Contains(kv.Values(m), Attribute{Value: "!"})) 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /ch/ch.go: -------------------------------------------------------------------------------- 1 | package ch 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Funnel returns a channel containing the values from all channels. 8 | func Funnel[T any](channels ...chan T) chan T { 9 | ret := make(chan T) 10 | 11 | var wg sync.WaitGroup 12 | wg.Add(len(channels)) 13 | 14 | for _, c := range channels { 15 | go func(c chan T) { 16 | for v := range c { 17 | ret <- v 18 | } 19 | 20 | wg.Done() 21 | }(c) 22 | } 23 | 24 | go func() { 25 | wg.Wait() 26 | close(ret) 27 | }() 28 | 29 | return ret 30 | } 31 | 32 | // Split implements a Fan-Out pattern. 33 | func Split[T any](ch <-chan T, n int) []<-chan T { 34 | if n <= 0 { 35 | panic("splitting is only possible for a positive number of channels") 36 | } 37 | 38 | ret := make([]<-chan T, 0, n) 39 | 40 | for i := 0; i < n; i++ { 41 | c := make(chan T) 42 | 43 | go func() { 44 | defer close(c) 45 | for v := range ch { 46 | c <- v 47 | } 48 | }() 49 | 50 | ret = append(ret, c) 51 | } 52 | 53 | return ret 54 | } 55 | -------------------------------------------------------------------------------- /ch/ch_test.go: -------------------------------------------------------------------------------- 1 | package ch 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/nikgalushko/fx/slice" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestFunnel(t *testing.T) { 13 | var channels []chan int 14 | for i := 0; i < 3; i++ { 15 | var arr []int 16 | for j := 0; j < 5; j++ { 17 | arr = append(arr, j) 18 | } 19 | 20 | channels = append(channels, createChanFrom(arr)) 21 | } 22 | 23 | ch := Funnel(channels...) 24 | sum := 0 25 | 26 | for v := range ch { 27 | sum += v 28 | } 29 | 30 | if sum != 30 { 31 | t.Fatalf("%d != 30", sum) 32 | } 33 | } 34 | 35 | func TestSplit(t *testing.T) { 36 | ch := createChanFrom([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}) 37 | 38 | splitted := Split(ch, 3) 39 | require.Equal(t, 3, len(splitted)) 40 | 41 | var wg sync.WaitGroup 42 | wg.Add(3) 43 | 44 | sum := new(int32) 45 | slice.Each(splitted, func(c <-chan int) { 46 | for v := range c { 47 | atomic.AddInt32(sum, int32(v)) 48 | } 49 | wg.Done() 50 | }) 51 | 52 | wg.Wait() 53 | require.Equal(t, int32(45), *sum) 54 | } 55 | 56 | func TestSplitPanic(t *testing.T) { 57 | ch := createChanFrom([]int{1, 2}) 58 | 59 | require.Panics(t, func() { Split(ch, 0) }) 60 | require.Panics(t, func() { Split(ch, -1) }) 61 | } 62 | 63 | func createChanFrom(arr []int) chan int { 64 | ret := make(chan int, len(arr)) 65 | for _, v := range arr { 66 | ret <- v 67 | } 68 | close(ret) 69 | 70 | return ret 71 | } 72 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikgalushko/fx/14e235b06e42ada639e0b1681a47677723788db4/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # fx 2 | Fx is a useful functional programming helpers. 3 | Support **only** Go 1.18+. 4 | 5 | ## Features 6 | - Slice 7 | - Each 8 | - Collect 9 | - Reduce 10 | - Find 11 | - Filter 12 | - Every 13 | - Some 14 | - Contains 15 | - Max 16 | - Min 17 | - GroupBy 18 | - Sample 19 | - SampleN 20 | - Union 21 | - Intersection 22 | - Uniq 23 | - IndexOf 24 | - LastIndexOf 25 | - Reverse 26 | - Map 27 | - Keys 28 | - Values 29 | - Each 30 | - Filter 31 | - Channel 32 | - Merge 33 | 34 | ## Documentation 35 | 36 | Documentation with examples can be found here: https://nikgalushko.github.io/fx/ 37 | 38 | ## Installation 39 | 40 | slice helpers `go get github.com/nikgalushko/fx/slice` 41 | 42 | map helpers `go get github.com/nikgalushko/fx/kv` 43 | 44 | channel helpers `go get github.com/nikgalushko/fx/ch` 45 | 46 | ## Usage 47 | 48 | ```go 49 | import ( 50 | "fmt" 51 | 52 | "github.com/nikgalushko/fx/kv" 53 | "github.com/nikgalushko/fx/slice" 54 | ) 55 | 56 | type ( 57 | ID string 58 | 59 | Attribute struct { 60 | Value string 61 | } 62 | ) 63 | 64 | func main() { 65 | m := map[ID]Attribute{ 66 | ID("1"): {Value: "blah"}, 67 | ID("1861"): {Value: "!"}, 68 | ID("1234"): {Value: "yeah"}, 69 | } 70 | 71 | fmt.Println("ids", kv.Keys(m), "contains special", slice.Contains(kv.Values(m), Attribute{Value: "!"})) 72 | } 73 | ``` -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | * [Home](README.md) 4 | * [slice](slice.md) 5 | * [map](map.md) 6 | * [channel](channel.md) 7 | -------------------------------------------------------------------------------- /docs/channel.md: -------------------------------------------------------------------------------- 1 | # Channel functions 2 | 3 | ## Funnel 4 | Funnel returns a channel containing the values from all channels. 5 | 6 | ## Split 7 | Split implements a Fan-Out pattern. 8 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/map.md: -------------------------------------------------------------------------------- 1 | # Map functions 2 | 3 | ## Keys 4 | Keys reurns a slice of keys from map. 5 | 6 | ``` 7 | keys := Keys(map[string]int{ 8 | "one": 1, 9 | "two": 2, 10 | "three": 3, 11 | }) 12 | 13 | 14 | // keys is []string{"one", "three", "two"}. The order may be different. 15 | ``` 16 | 17 | ## Values 18 | Values reurns a slice of values from map. 19 | 20 | ``` 21 | values := Values(map[int]int{ 22 | 0: 10, 23 | 1: 11, 24 | 2: 12, 25 | }) 26 | 27 | // values is []int{10, 11, 12}. The order may be different. 28 | ``` 29 | 30 | ## Each 31 | Each calls the function on each key-value pair of map. 32 | 33 | ``` 34 | m := map[int]string{ 35 | 1: "a", 36 | 2: "b", 37 | 3: "c", 38 | 4: "d", 39 | } 40 | 41 | var ( 42 | keys []int 43 | values []string 44 | ) 45 | Each(m, func(key int, value string) { 46 | if key > 3 { 47 | return 48 | } 49 | 50 | keys = append(keys, key) 51 | values = append(values, value) 52 | }) 53 | 54 | // keys is []int{1, 2, 3}. The order may be different. 55 | // values is []string{"a", "b", "c"}. The order may be different. 56 | ``` 57 | 58 | ## Filter 59 | Filter returns a new map that contains key-value pairs that mathch the condition. 60 | 61 | ``` 62 | m := map[int]string{ 63 | 1: "one", 64 | 2: "two", 65 | 5: "five", 66 | 6: "six", 67 | 7: "seven", 68 | } 69 | 70 | filtered := Filter(m, func(key int, value string) bool { 71 | return key < 3 || len(value) < 4 72 | }) 73 | 74 | // filtered is map[int]string{1: "one", 2: "two", 6: "six"}. The order may be different. Original map is not changed. 75 | ``` -------------------------------------------------------------------------------- /docs/slice.md: -------------------------------------------------------------------------------- 1 | # Slice functions 2 | 3 | ## Each 4 | Each calls the function on each item in the slice. 5 | 6 | ``` 7 | sum := 0 8 | Each([]int{1, 2, 3, 4}, func(i int) { 9 | sum += i 10 | }) 11 | 12 | // sum is 10 13 | ``` 14 | ## Collect 15 | Collect returns a new slice of values by mapping each value of original slice through a transformation function. 16 | 17 | ``` 18 | arr := Collect([]int{1, 2, 3, 4}, func(i int) int { 19 | return i * 2 20 | }) 21 | 22 | // arr is []int{2, 4, 6, 8} 23 | ``` 24 | ## Reduce 25 | Reduce reduces a slice of values to single value. 26 | 27 | ``` 28 | join := Reduce([]string{"b", "l", "a", "h"}, func(memo, s string) string { 29 | return memo + s 30 | }, "") 31 | 32 | // join is "blah" 33 | ``` 34 | 35 | ## Find 36 | Find returns the first element in the slice that matches the condition. If slice doesn't contain an element it returns a default type value and false as second value. 37 | 38 | ``` 39 | element, ok := Find([]int{1, 2, 3, 4, 5}, func(i int) bool { return i == 4 }) 40 | 41 | // element is 4; ok is true 42 | ``` 43 | 44 | ## Filter 45 | Filter returns all elements in the slice that mathch the condition. 46 | 47 | ``` 48 | ret := Filter([]int{10, 1, 4, 20, 5, 2}, func(i int) bool { return i < 10 }) 49 | 50 | // ret is []int{1, 4, 5, 2} 51 | ``` 52 | 53 | ## Every 54 | Every returns true if all elements match the condition. 55 | 56 | ``` 57 | ret := Every([]int{10, 1, 4, 20, 5, 2}, func(i int) bool { return i >= 0 }) 58 | 59 | // ret is true 60 | ``` 61 | 62 | ## Some 63 | Some returns true if there is at least one element that satisfies the condition. 64 | 65 | ``` 66 | ret := Some([]int{10, 1, 4, 20, 5, 2}, func(i int) bool { return i < 0 }) 67 | 68 | // ret is false 69 | ``` 70 | 71 | ## Contains 72 | Contains returns true if value is present in the slice. 73 | 74 | ``` 75 | ret := Contains([]int{1, 2, 10, 23, 4}, 4) 76 | 77 | // ret is true 78 | ``` 79 | 80 | ## Max 81 | Max returns the maximum value from the slice. 82 | If input slice is empty it returns a default value for input type. 83 | 84 | ``` 85 | arr := []int{10, 2, 1, 4, 19} 86 | ret := Max(arr) 87 | 88 | // ret is 19 89 | 90 | zero := Max([]int{}) 91 | 92 | // zero is 0 93 | ``` 94 | 95 | ## Min 96 | Min returns the minimum value from the slice. 97 | If input slice is empty it returns a default value for input type. 98 | 99 | ``` 100 | arr := []int{10, 2, 1, 4, 19} 101 | ret := Min(arr) 102 | 103 | // ret is 11 104 | 105 | zero := Max([]string{}) 106 | 107 | // zero is "" 108 | ``` 109 | 110 | ## GroupBy 111 | GroupBy splits the slice into groups, grouped by the result of the function call. 112 | 113 | ``` 114 | group := GroupBy([]string{"one", "two", "three"}, func(s string) int { return len(s) }) 115 | 116 | // group is map[int][]string{3: {"one", "two"}, 5: {"three"}} 117 | ``` 118 | 119 | ## Sample 120 | Sample returns the random element from slice. 121 | 122 | ``` 123 | v := Sample([]int{1, 2, 3, 4}) 124 | 125 | // v is random element from {1, 2, 3, 4} 126 | ``` 127 | 128 | ## SampleN 129 | SampleN returns the N random elements from slice. 130 | 131 | ``` 132 | arr := []int{11, 12, 13, 14, 15, 16, 17, 18, 19} 133 | samples := SampleN(arr, 5) 134 | 135 | // samples is slice of random elements from arr 136 | ``` 137 | 138 | ## Union 139 | Union returns a slice of unique values from passed slices. 140 | 141 | ``` 142 | ret := Union([]string{"a", "b", "c"}, []string{"b", "c", "d"}} 143 | 144 | // ret is []string{"a", "b", "c", "d"} 145 | ``` 146 | 147 | ## Intersection 148 | Intersection returns a slice of values that are in all passed slices. 149 | 150 | ``` 151 | arr1 := []string{"a", "b", "c"} 152 | arr2 := []string{"b", "c", "d"} 153 | 154 | ret := Intersection(arr1, arr2) 155 | 156 | // ret is []string{"b", "c"} 157 | ``` 158 | 159 | ## Uniq 160 | Uniq returns a slice of unique values. 161 | 162 | ``` 163 | arr := []string{"a", "b", "a", "c", "b"} 164 | ret := Uniq(arr) 165 | 166 | // ret is []string{"a", "b", "c"} 167 | ``` 168 | 169 | ## IndexOf 170 | IndexOf returns first index of the found element in the slice. If slice doesn't contain an element it returns -1. 171 | 172 | ``` 173 | i := IndexOf([]int{1, 2, 3, 2}, 2) 174 | 175 | // i is 1 176 | ``` 177 | 178 | ## LastIndexOf 179 | LastIndexOf like as IndexOf, but the search goes from the end. 180 | 181 | ``` 182 | i := LastIndexOf([]int{1, 2, 3, 4}, 20) 183 | 184 | // i is -1 185 | ``` 186 | 187 | ## Reduce 188 | Reverse reverses the order of the elements in place. 189 | 190 | ``` 191 | arr := []int{0, 1, 2, 3} 192 | Reverse(arr) 193 | 194 | // arr is []int{3, 2, 1, 0} 195 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nikgalushko/fx 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.0 // indirect 7 | github.com/pmezard/go-difflib v1.0.0 // indirect 8 | github.com/stretchr/testify v1.7.0 // indirect 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /kv/kv.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | // Keys reurns a slice of keys from map. 4 | func Keys[K comparable, V any](m map[K]V) []K { 5 | ret := make([]K, 0, len(m)) 6 | 7 | for k := range m { 8 | ret = append(ret, k) 9 | } 10 | 11 | return ret 12 | } 13 | 14 | // Values reurns a slice of values from map. 15 | func Values[K comparable, V any](m map[K]V) []V { 16 | ret := make([]V, 0, len(m)) 17 | 18 | for _, v := range m { 19 | ret = append(ret, v) 20 | } 21 | 22 | return ret 23 | } 24 | 25 | // Each calls the function on each key-value pair of map. 26 | func Each[K comparable, V any](m map[K]V, f func(key K, value V)) { 27 | for k, v := range m { 28 | f(k, v) 29 | } 30 | } 31 | 32 | // Filter returns a new map that contains key-value pairs that mathch the condition. 33 | func Filter[K comparable, V any](m map[K]V, f func(key K, value V) bool) map[K]V { 34 | ret := make(map[K]V) 35 | 36 | for k, v := range m { 37 | if f(k, v) { 38 | ret[k] = v 39 | } 40 | } 41 | 42 | return ret 43 | } 44 | -------------------------------------------------------------------------------- /kv/kv_test.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestKeys(t *testing.T) { 11 | keys := Keys(map[string]int{ 12 | "one": 1, 13 | "two": 2, 14 | "three": 3, 15 | }) 16 | 17 | sort.Strings(keys) 18 | 19 | require.Equal(t, []string{"one", "three", "two"}, keys) 20 | } 21 | 22 | func TestValues(t *testing.T) { 23 | values := Values(map[int]int{ 24 | 0: 10, 25 | 1: 11, 26 | 2: 12, 27 | }) 28 | 29 | sort.Ints(values) 30 | 31 | require.Equal(t, []int{10, 11, 12}, values) 32 | } 33 | 34 | func TestEach(t *testing.T) { 35 | var ( 36 | keys []int 37 | values []string 38 | ) 39 | 40 | Each(map[int]string{ 41 | 1: "a", 42 | 2: "b", 43 | 3: "c", 44 | 4: "d", 45 | }, func(key int, value string) { 46 | if key > 3 { 47 | return 48 | } 49 | 50 | keys = append(keys, key) 51 | values = append(values, value) 52 | }) 53 | 54 | sort.Ints(keys) 55 | sort.Strings(values) 56 | 57 | require.Equal(t, []int{1, 2, 3}, keys) 58 | require.Equal(t, []string{"a", "b", "c"}, values) 59 | } 60 | 61 | func TestFilter(t *testing.T) { 62 | m := map[int]string{ 63 | 1: "one", 64 | 2: "two", 65 | 5: "five", 66 | 6: "six", 67 | 7: "seven", 68 | } 69 | 70 | filtered := Filter(m, func(key int, value string) bool { 71 | return key < 3 || len(value) < 4 72 | }) 73 | 74 | require.Equal(t, map[int]string{1: "one", 2: "two", 6: "six"}, filtered) 75 | require.Len(t, m, 5) 76 | } 77 | -------------------------------------------------------------------------------- /slice/slice.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "constraints" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | rand.Seed(time.Now().UnixNano()) 11 | } 12 | 13 | // Each calls the function on each item in the slice. 14 | func Each[A ~[]T, T any](arr A, f func(T)) { 15 | for _, v := range arr { 16 | f(v) 17 | } 18 | } 19 | 20 | // Collect returns a new slice of values by mapping each value of original slice through a transformation function. 21 | func Collect[A ~[]T, T any, M any](arr A, f func(T) M) []M { 22 | ret := make([]M, len(arr)) 23 | 24 | for i, v := range arr { 25 | ret[i] = f(v) 26 | } 27 | 28 | return ret 29 | } 30 | 31 | // Reduce reduces a slice of values to single value. 32 | func Reduce[A ~[]T, T any, M any](arr A, f func(M, T) M, initial M) M { 33 | for _, v := range arr { 34 | initial = f(initial, v) 35 | } 36 | 37 | return initial 38 | } 39 | 40 | // Find returns the first element in the slice that matches the condition. 41 | // If slice doesn't contain an element it returns a default type value and false as second value. 42 | func Find[A ~[]T, T any](arr A, f func(T) bool) (T, bool) { 43 | for _, v := range arr { 44 | if f(v) { 45 | return v, true 46 | } 47 | } 48 | 49 | return defaultvalue[T](), false 50 | } 51 | 52 | // Filter returns all elements in the slice that mathch the condition. 53 | func Filter[A ~[]T, T any](arr A, f func(T) bool) A { 54 | var ret A 55 | 56 | for _, v := range arr { 57 | if f(v) { 58 | ret = append(ret, v) 59 | } 60 | } 61 | 62 | return ret 63 | } 64 | 65 | // Every returns true if all elements match the condition. 66 | func Every[A ~[]T, T any](arr A, f func(T) bool) bool { 67 | for _, v := range arr { 68 | if !f(v) { 69 | return false 70 | } 71 | } 72 | 73 | return true 74 | } 75 | 76 | // Some returns true if there is at least one element that satisfies the condition. 77 | func Some[A ~[]T, T any](arr A, f func(T) bool) bool { 78 | for _, v := range arr { 79 | if f(v) { 80 | return true 81 | } 82 | } 83 | 84 | return false 85 | } 86 | 87 | // Contains returns true if value is present in the slice. 88 | func Contains[A ~[]T, T comparable](arr A, value T) bool { 89 | for _, v := range arr { 90 | if v == value { 91 | return true 92 | } 93 | } 94 | 95 | return false 96 | } 97 | 98 | // Max returns the maximum value from the slice. 99 | // If input slice is empty it returns a default value for input type. 100 | func Max[A ~[]T, T constraints.Ordered](arr A) T { 101 | if len(arr) == 0 { 102 | return defaultvalue[T]() 103 | } 104 | 105 | e := arr[0] 106 | 107 | for i := 1; i < len(arr); i++ { 108 | if arr[i] > e { 109 | e = arr[i] 110 | } 111 | } 112 | 113 | return e 114 | } 115 | 116 | // Min returns the minimum value from the slice. 117 | // If input slice is empty it returns a default value for input type. 118 | func Min[A ~[]T, T constraints.Ordered](arr A) T { 119 | if len(arr) == 0 { 120 | return defaultvalue[T]() 121 | } 122 | 123 | e := arr[0] 124 | 125 | for i := 1; i < len(arr); i++ { 126 | if arr[i] < e { 127 | e = arr[i] 128 | } 129 | } 130 | 131 | return e 132 | } 133 | 134 | // GroupBy splits the slice into groups, grouped by the result of the function call. 135 | func GroupBy[A ~[]T, T any, M comparable](arr A, f func(T) M) map[M]A { 136 | ret := make(map[M]A) 137 | 138 | for _, v := range arr { 139 | m := f(v) 140 | ret[m] = append(ret[m], v) 141 | } 142 | 143 | return ret 144 | } 145 | 146 | // Sample returns the random element from slice. 147 | func Sample[A ~[]T, T any](arr A) T { 148 | return arr[rand.Intn(len(arr))] 149 | } 150 | 151 | // SampleN returns the N random elements from slice. 152 | func SampleN[A ~[]T, T any](arr A, n int) []T { 153 | if n < 0 { 154 | return A{} 155 | } 156 | 157 | if n > len(arr) { 158 | n = len(arr) 159 | } 160 | 161 | ret := make([]T, n) 162 | for i, v := range rand.Perm(n) { 163 | ret[i] = arr[v] 164 | } 165 | 166 | return ret 167 | } 168 | 169 | // Union returns a slice of unique values from passed slices. 170 | func Union[A ~[]T, T comparable](arr ...A) A { 171 | if len(arr) == 0 { 172 | return A{} 173 | } 174 | 175 | if len(arr) == 1 { 176 | return arr[0] 177 | } 178 | 179 | ret := make(A, 0, len(arr[0])) 180 | m := make(map[T]struct{}) 181 | for _, array := range arr { 182 | for i := 0; i < len(array); i++ { 183 | if _, ok := m[array[i]]; !ok { 184 | ret = append(ret, array[i]) 185 | m[array[i]] = struct{}{} 186 | } 187 | } 188 | } 189 | 190 | return ret 191 | } 192 | 193 | // Intersection returns a slice of values that are in all passed slices. 194 | func Intersection[A ~[]T, T comparable](arr ...A) A { 195 | if len(arr) == 0 { 196 | return A{} 197 | } 198 | 199 | if len(arr) == 1 { 200 | return arr[0] 201 | } 202 | 203 | ret := arr[0] 204 | arr = arr[1:] 205 | 206 | for len(arr) != 0 { 207 | var nextPath A 208 | 209 | part2 := arr[0] 210 | m := make(map[T]struct{}) 211 | 212 | for _, array := range []A{ret, part2} { 213 | for i := 0; i < len(array); i++ { 214 | if _, ok := m[array[i]]; ok { 215 | nextPath = append(nextPath, array[i]) 216 | } else { 217 | m[array[i]] = struct{}{} 218 | } 219 | } 220 | } 221 | 222 | ret = nextPath 223 | arr = arr[1:] 224 | } 225 | 226 | return ret 227 | } 228 | 229 | // Uniq returns a slice of unique values. 230 | func Uniq[A ~[]T, T comparable](arr A) []T { 231 | ret := make(A, 0) 232 | m := make(map[T]struct{}) 233 | 234 | for _, elem := range arr { 235 | if _, ok := m[elem]; !ok { 236 | m[elem] = struct{}{} 237 | ret = append(ret, elem) 238 | } 239 | } 240 | 241 | return ret 242 | } 243 | 244 | // IndexOf returns first index of the found element in the slice. 245 | // If slice doesn't contain an element it returns -1. 246 | func IndexOf[A ~[]T, T comparable](arr A, value T) int { 247 | for i := 0; i < len(arr); i++ { 248 | if arr[i] == value { 249 | return i 250 | } 251 | } 252 | 253 | return -1 254 | } 255 | 256 | // LastIndexOf like as IndexOf, but the search goes from the end. 257 | func LastIndexOf[A ~[]T, T comparable](arr A, value T) int { 258 | for i := len(arr) - 1; i >= 0; i-- { 259 | if arr[i] == value { 260 | return i 261 | } 262 | } 263 | 264 | return -1 265 | } 266 | 267 | // Reverse reverses the order of the elements in place. 268 | func Reverse[A ~[]T, T any](arr A) { 269 | for i := 0; i < len(arr)/2; i++ { 270 | arr[i], arr[len(arr)-i-1] = arr[len(arr)-i-1], arr[i] 271 | } 272 | } 273 | 274 | func defaultvalue[T any]() T { 275 | n := new(T) 276 | return *n 277 | } -------------------------------------------------------------------------------- /slice/slice_test.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestEach(t *testing.T) { 10 | sum := 0 11 | Each([]int{1, 2, 3, 4}, func(i int) { 12 | sum += i 13 | }) 14 | 15 | require.Equal(t, 10, sum) 16 | } 17 | 18 | func TestCollect(t *testing.T) { 19 | arr := Collect([]int{1, 2, 3, 4}, func(i int) int { 20 | return i * 2 21 | }) 22 | 23 | require.Equal(t, []int{2, 4, 6, 8}, arr) 24 | } 25 | 26 | func TestReduce(t *testing.T) { 27 | join := Reduce([]string{"b", "l", "a", "h"}, func(memo, s string) string { 28 | return memo + s 29 | }, "") 30 | 31 | require.Equal(t, "blah", join) 32 | } 33 | 34 | func TestFind(t *testing.T) { 35 | tests := map[string]struct { 36 | arr []int 37 | f func(int) bool 38 | element int 39 | ok bool 40 | }{ 41 | "found": { 42 | arr: []int{1, 2, 3, 4, 5}, 43 | f: func(i int) bool { return i == 4 }, 44 | element: 4, 45 | ok: true, 46 | }, 47 | "not found": { 48 | arr: []int{1, 2, 3, 4, 5}, 49 | f: func(i int) bool { return i == 100 }, 50 | element: 0, 51 | ok: false, 52 | }, 53 | } 54 | 55 | for title, tt := range tests { 56 | element, ok := Find(tt.arr, tt.f) 57 | 58 | require.Equal(t, tt.element, element, title) 59 | require.Equal(t, tt.ok, ok, title) 60 | } 61 | } 62 | 63 | func TestFilter(t *testing.T) { 64 | ret := Filter([]int{10, 1, 4, 20, 5, 2}, func(i int) bool { return i < 10 }) 65 | 66 | require.Equal(t, []int{1, 4, 5, 2}, ret) 67 | } 68 | 69 | func TestEvery(t *testing.T) { 70 | tests := map[string]struct { 71 | arr []int 72 | f func(int) bool 73 | expected bool 74 | }{ 75 | "not ok": { 76 | arr: []int{10, 1, 4, 20, 5, 2}, 77 | f: func(i int) bool { return i >= 10 }, 78 | expected: false, 79 | }, 80 | "ok": { 81 | arr: []int{10, 1, 4, 20, 5, 2}, 82 | f: func(i int) bool { return i >= 0 }, 83 | expected: true, 84 | }, 85 | } 86 | 87 | for title, tt := range tests { 88 | require.Equal(t, tt.expected, Every(tt.arr, tt.f), title) 89 | } 90 | } 91 | 92 | func TestSome(t *testing.T) { 93 | tests := map[string]struct { 94 | arr []int 95 | f func(int) bool 96 | expected bool 97 | }{ 98 | "not ok": { 99 | arr: []int{10, 1, 4, 20, 5, 2}, 100 | f: func(i int) bool { return i < 0 }, 101 | expected: false, 102 | }, 103 | "ok": { 104 | arr: []int{10, 1, 4, 20, 5, 2}, 105 | f: func(i int) bool { return i >= 10 }, 106 | expected: true, 107 | }, 108 | } 109 | 110 | for title, tt := range tests { 111 | require.Equal(t, tt.expected, Some(tt.arr, tt.f), title) 112 | } 113 | } 114 | 115 | func TestContains(t *testing.T) { 116 | tests := map[string]struct { 117 | arr []int 118 | value int 119 | expected bool 120 | }{ 121 | "contains": { 122 | arr: []int{1, 2, 10, 23, 4}, 123 | value: 4, 124 | expected: true, 125 | }, 126 | "doesn't contain": { 127 | arr: []int{1, 2, 10, 23, 4}, 128 | value: 423, 129 | expected: false, 130 | }, 131 | } 132 | 133 | for title, tt := range tests { 134 | require.Equal(t, tt.expected, Contains(tt.arr, tt.value), title) 135 | } 136 | } 137 | 138 | func TestGroupBy(t *testing.T) { 139 | group := GroupBy([]string{"one", "two", "three"}, func(s string) int { return len(s) }) 140 | 141 | require.Equal(t, map[int][]string{3: {"one", "two"}, 5: {"three"}}, group) 142 | } 143 | 144 | func TestSample(t *testing.T) { 145 | arr := []int{1, 2, 3, 4} 146 | v := Sample(arr) 147 | 148 | require.Equal(t, true, Contains(arr, v)) 149 | } 150 | 151 | func TestSampleN(t *testing.T) { 152 | check := func(arr, samples []int){ 153 | require.Equal(t, len(Uniq(samples)), len(samples)) 154 | 155 | for _, v := range samples { 156 | require.Equal(t, true, Contains(arr, v)) 157 | } 158 | } 159 | 160 | tests := []struct{ 161 | input []int 162 | n int 163 | }{ 164 | { 165 | input: []int{11, 12, 13, 14, 15, 16, 17, 18, 19}, 166 | n: 5, 167 | }, 168 | { 169 | input: []int{0, 1, 2}, 170 | n: 1, 171 | }, 172 | { 173 | input: []int{0, 1, 2}, 174 | n: 100, 175 | }, 176 | { 177 | input: []int{0, 1, 2}, 178 | n: -10, 179 | }, 180 | } 181 | 182 | for _, tt := range tests { 183 | check(tt.input, SampleN(tt.input, tt.n)) 184 | } 185 | } 186 | 187 | func TestUnion(t *testing.T) { 188 | tests := map[string]struct { 189 | in [][]string 190 | expected []string 191 | }{ 192 | "two arrays": { 193 | in: [][]string{{"a", "b", "c"}, {"b", "c", "d"}}, 194 | expected: []string{"a", "b", "c", "d"}, 195 | }, 196 | "zero arrays": { 197 | in: nil, 198 | expected: []string{}, 199 | }, 200 | "one array": { 201 | in: [][]string{{"1", "2", "3"}}, 202 | expected: []string{"1", "2", "3"}, 203 | }, 204 | "three arrays": { 205 | in: [][]string{{"a", "b", "c"}, {"b", "c", "d"}, {"c", "d", "e"}}, 206 | expected: []string{"a", "b", "c", "d", "e"}, 207 | }, 208 | } 209 | 210 | for title, tt := range tests { 211 | require.Equal(t, tt.expected, Union(tt.in...), title) 212 | } 213 | } 214 | 215 | func TestIntersection(t *testing.T) { 216 | tests := map[string]struct{ 217 | in [][]string 218 | expected []string 219 | }{ 220 | "without arrays": { 221 | in: [][]string{}, 222 | expected: []string{}, 223 | }, 224 | "single array": { 225 | in: [][]string{{"a", "a", "b"}}, 226 | expected: []string{"a", "a", "b"}, 227 | }, 228 | "two arrays": { 229 | in: [][]string{{"a", "b", "c"}, {"b", "c", "d"}}, 230 | expected: []string{"b", "c"}, 231 | }, 232 | "three arrays": { 233 | in: [][]string{{"a", "b", "c"}, {"b", "c", "d"}, {"a", "f", "g", "c"}}, 234 | expected: []string{"c"}, 235 | }, 236 | "four arrays": { 237 | in: [][]string{{"a", "b", "c"}, {"b", "c", "d"}, {"a", "f", "g", "c"}, {"k", "v"}}, 238 | expected: nil, 239 | }, 240 | } 241 | 242 | for title, tt := range tests { 243 | require.Equal(t, tt.expected, Intersection(tt.in...), title) 244 | } 245 | } 246 | 247 | func TestIndexOf(t *testing.T) { 248 | require.Equal(t, 1, IndexOf([]int{1, 2, 3, 2}, 2), "found") 249 | require.Equal(t, -1, IndexOf([]int{1, 2, 3, 4}, 20), "not found") 250 | } 251 | 252 | func TestLastIndexOf(t *testing.T) { 253 | require.Equal(t, 3, LastIndexOf([]int{1, 2, 3, 2}, 2), "found") 254 | require.Equal(t, -1, LastIndexOf([]int{1, 2, 3, 4}, 20), "not found") 255 | } 256 | 257 | func TestMinMax(t *testing.T) { 258 | tests := map[string]struct{ 259 | in []int 260 | max int 261 | min int 262 | }{ 263 | "simple array": { 264 | in: []int{10, 2, 1, 4, 19}, 265 | max: 19, 266 | min: 1, 267 | }, 268 | "zero array": { 269 | in : nil, 270 | max: 0, 271 | min: 0, 272 | }, 273 | } 274 | 275 | for title, tt := range tests { 276 | require.Equal(t, tt.max, Max(tt.in), title) 277 | require.Equal(t, tt.min, Min(tt.in), title) 278 | } 279 | } 280 | 281 | func TestReverse(t *testing.T) { 282 | tests := map[string]struct { 283 | in []int 284 | expected []int 285 | }{ 286 | "even": { 287 | in: []int{0, 1, 2, 3}, 288 | expected: []int{3, 2, 1, 0}, 289 | }, 290 | "odd": { 291 | in: []int{0, 1, 2, 3, 4}, 292 | expected: []int{4, 3, 2, 1, 0}, 293 | }, 294 | } 295 | 296 | for title, tt := range tests { 297 | Reverse(tt.in) 298 | require.Equal(t, tt.expected, tt.in, title) 299 | } 300 | } 301 | --------------------------------------------------------------------------------