From a20530ddfd8f0c1f4b0c4ba8a67977a92b6d0e60 Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Mon, 27 Jan 2025 13:19:52 +1100 Subject: [PATCH] Initial commit of modash This was taken from github.com/lmika/gopkgs/fp --- .forgeio/workflows/ci.yaml | 23 +++++++++++++++ .gitignore | 1 + README.md | 3 ++ go.mod | 10 +++++++ go.sum | 9 ++++++ momap/fromslice.go | 30 ++++++++++++++++++++ momap/keys.go | 9 ++++++ momap/toslice.go | 29 +++++++++++++++++++ momap/toslice_test.go | 28 ++++++++++++++++++ momap/values.go | 17 +++++++++++ moslice/filter.go | 18 ++++++++++++ moslice/filter_test.go | 29 +++++++++++++++++++ moslice/find.go | 21 ++++++++++++++ moslice/find_test.go | 25 ++++++++++++++++ moslice/flatten.go | 19 +++++++++++++ moslice/foreach.go | 13 +++++++++ moslice/map.go | 33 ++++++++++++++++++++++ moslice/map_test.go | 58 ++++++++++++++++++++++++++++++++++++++ moslice/uniq.go | 19 +++++++++++++ moslice/uniq_test.go | 31 ++++++++++++++++++++ 20 files changed, 425 insertions(+) create mode 100644 .forgeio/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 momap/fromslice.go create mode 100644 momap/keys.go create mode 100644 momap/toslice.go create mode 100644 momap/toslice_test.go create mode 100644 momap/values.go create mode 100644 moslice/filter.go create mode 100644 moslice/filter_test.go create mode 100644 moslice/find.go create mode 100644 moslice/find_test.go create mode 100644 moslice/flatten.go create mode 100644 moslice/foreach.go create mode 100644 moslice/map.go create mode 100644 moslice/map_test.go create mode 100644 moslice/uniq.go create mode 100644 moslice/uniq_test.go diff --git a/.forgeio/workflows/ci.yaml b/.forgeio/workflows/ci.yaml new file mode 100644 index 0000000..6b8c164 --- /dev/null +++ b/.forgeio/workflows/ci.yaml @@ -0,0 +1,23 @@ +--- +name: 'ci' + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: docker + steps: + - name: Cloning repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: '1.22.4' + - name: Test + run: | + go test ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c09d72 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# modash + +A lodash-inspired utility package. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4fa4794 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module lmika.dev/pkg/modash + +go 1.23.3 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fe99d71 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/momap/fromslice.go b/momap/fromslice.go new file mode 100644 index 0000000..ad6396b --- /dev/null +++ b/momap/fromslice.go @@ -0,0 +1,30 @@ +package momap + +func FromSliceGroups[T any, K comparable](ts []T, fn func(t T) K) map[K][]T { + kvs := make(map[K][]T) + for _, t := range ts { + k := fn(t) + kvs[k] = append(kvs[k], t) + } + return kvs +} + +func FromSlice[T any, K comparable, V any](ts []T, fn func(t T) (K, V)) map[K]V { + m, _ := FromSliceWithError(ts, func(t T) (k K, v V, _ error) { + k, v = fn(t) + return k, v, nil + }) + return m +} + +func FromSliceWithError[T any, K comparable, V any](ts []T, fn func(t T) (K, V, error)) (map[K]V, error) { + kvs := make(map[K]V) + for _, t := range ts { + k, v, err := fn(t) + if err != nil { + return nil, err + } + kvs[k] = v + } + return kvs, nil +} diff --git a/momap/keys.go b/momap/keys.go new file mode 100644 index 0000000..6fe239f --- /dev/null +++ b/momap/keys.go @@ -0,0 +1,9 @@ +package momap + +func Keys[K comparable, V any](m map[K]V) []K { + ks := make([]K, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + return ks +} diff --git a/momap/toslice.go b/momap/toslice.go new file mode 100644 index 0000000..0a0393c --- /dev/null +++ b/momap/toslice.go @@ -0,0 +1,29 @@ +package momap + +func ToSlice[K comparable, V, T any](m map[K]V, fn func(k K, v V) T) []T { + if m == nil { + return nil + } + + ts := make([]T, 0, len(m)) + for k, v := range m { + ts = append(ts, fn(k, v)) + } + return ts +} + +func ToSliceWithError[K comparable, V, T any](m map[K]V, fn func(k K, v V) (T, error)) ([]T, error) { + if m == nil { + return nil, nil + } + + ts := make([]T, 0, len(m)) + for k, v := range m { + w, err := fn(k, v) + if err != nil { + return nil, err + } + ts = append(ts, w) + } + return ts, nil +} diff --git a/momap/toslice_test.go b/momap/toslice_test.go new file mode 100644 index 0000000..9713453 --- /dev/null +++ b/momap/toslice_test.go @@ -0,0 +1,28 @@ +package momap_test + +import ( + "lmika.dev/pkg/modash/momap" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToSlice(t *testing.T) { + type pair struct { + left int + right string + } + + ms := map[int]string{ + 1: "one", + 2: "two", + 3: "three", + } + + pairs := momap.ToSlice(ms, func(k int, v string) pair { return pair{k, v} }) + + assert.Len(t, pairs, 3) + assert.Contains(t, pairs, pair{1, "one"}) + assert.Contains(t, pairs, pair{2, "two"}) + assert.Contains(t, pairs, pair{3, "three"}) +} diff --git a/momap/values.go b/momap/values.go new file mode 100644 index 0000000..e05d22c --- /dev/null +++ b/momap/values.go @@ -0,0 +1,17 @@ +package momap + +func Values[K comparable, V any](m map[K]V) []V { + vs := make([]V, 0, len(m)) + for _, v := range m { + vs = append(vs, v) + } + return vs +} + +func MapValues[K comparable, V any, W any](m map[K]V, fn func(v V, k K) W) map[K]W { + ws := make(map[K]W) + for k, v := range m { + ws[k] = fn(v, k) + } + return ws +} diff --git a/moslice/filter.go b/moslice/filter.go new file mode 100644 index 0000000..cd3bbbb --- /dev/null +++ b/moslice/filter.go @@ -0,0 +1,18 @@ +package moslice + +// Filter returns a slice containing all the elements of ts for which the passed in +// predicate returns true. If no items match the predicate, the function will return +// an empty slice. If ts is nil, the function will also return nil. +func Filter[T any](ts []T, predicate func(t T) bool) []T { + if ts == nil { + return nil + } + + filteredTs := make([]T, 0) + for _, t := range ts { + if predicate(t) { + filteredTs = append(filteredTs, t) + } + } + return filteredTs +} diff --git a/moslice/filter_test.go b/moslice/filter_test.go new file mode 100644 index 0000000..1d4ae28 --- /dev/null +++ b/moslice/filter_test.go @@ -0,0 +1,29 @@ +package moslice_test + +import ( + "lmika.dev/pkg/modash/moslice" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFilter(t *testing.T) { + var ( + ints = []int{1, 2, 3, 4, 5} + strs = []string{"foo", "bar", "baz"} + ) + + t.Run("should filter items matching the predicate", func(t *testing.T) { + assert.Equal(t, []int{2, 4}, moslice.Filter(ints, func(x int) bool { return x%2 == 0 })) + assert.Equal(t, []string{"bar", "baz"}, moslice.Filter(strs, func(x string) bool { return strings.Contains(x, "b") })) + }) + + t.Run("should moslice nil if the passed in slice is nil", func(t *testing.T) { + assert.Nil(t, moslice.Filter(nil, func(x int) bool { return x%2 == 0 })) + }) + + t.Run("should return empty slice if the passed in slice is empty slice", func(t *testing.T) { + assert.Equal(t, []int{}, moslice.Filter([]int{}, func(x int) bool { return x%2 == 0 })) + }) +} diff --git a/moslice/find.go b/moslice/find.go new file mode 100644 index 0000000..9971100 --- /dev/null +++ b/moslice/find.go @@ -0,0 +1,21 @@ +package moslice + +func Contains[T comparable](ts []T, needle T) bool { + for _, t := range ts { + if t == needle { + return true + } + } + return false +} + +func FindWhere[T comparable](ts []T, predicate func(t T) bool) (T, bool) { + var zeroT T + + for _, t := range ts { + if predicate(t) { + return t, true + } + } + return zeroT, false +} diff --git a/moslice/find_test.go b/moslice/find_test.go new file mode 100644 index 0000000..8bd76b5 --- /dev/null +++ b/moslice/find_test.go @@ -0,0 +1,25 @@ +package moslice_test + +import ( + "lmika.dev/pkg/modash/moslice" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContains(t *testing.T) { + var ( + ints = []int{1, 2, 3} + strs = []string{"a", "b", "c"} + ) + + t.Run("should find items in the slice", func(t *testing.T) { + assert.True(t, moslice.Contains(ints, 2)) + assert.True(t, moslice.Contains(strs, "c")) + }) + + t.Run("should return false if items not in slice", func(t *testing.T) { + assert.False(t, moslice.Contains(ints, 131)) + assert.False(t, moslice.Contains(strs, "bla")) + }) +} diff --git a/moslice/flatten.go b/moslice/flatten.go new file mode 100644 index 0000000..add9c34 --- /dev/null +++ b/moslice/flatten.go @@ -0,0 +1,19 @@ +package moslice + +func Flatten[T any](tss [][]T) []T { + if len(tss) == 0 { + return nil + } + + entireLen := 0 + for _, ts := range tss { + entireLen += len(ts) + } + + newTs := make([]T, 0, entireLen) + for _, ts := range tss { + newTs = append(newTs, ts...) + } + + return newTs +} diff --git a/moslice/foreach.go b/moslice/foreach.go new file mode 100644 index 0000000..55bc1e0 --- /dev/null +++ b/moslice/foreach.go @@ -0,0 +1,13 @@ +package moslice + +// ForEachWithError runs the passed in function for each element of T. If an error +// is encountered, the error is returned immediately and any subsequence elements +// will not be processed. +func ForEachWithError[T any](ts []T, fn func(t T) error) error { + for _, t := range ts { + if err := fn(t); err != nil { + return err + } + } + return nil +} diff --git a/moslice/map.go b/moslice/map.go new file mode 100644 index 0000000..78a9451 --- /dev/null +++ b/moslice/map.go @@ -0,0 +1,33 @@ +package moslice + +// Map returns a new slice containing the elements of ts transformed by the passed in function. +func Map[T, U any](ts []T, fn func(t T) U) (us []U) { + if ts == nil { + return nil + } + + us = make([]U, len(ts)) + for i, t := range ts { + us[i] = fn(t) + } + return us +} + +// Map returns a new slice containing the elements of ts transformed by the passed in function, which +// can either either a mapped value of U, or an error. If the mapping function returns an error, MapWithError +// will return nil and the returned error. +func MapWithError[T, U any](ts []T, fn func(t T) (U, error)) (us []U, err error) { + if ts == nil { + return nil, nil + } + + us = make([]U, len(ts)) + for i, t := range ts { + var e error + us[i], e = fn(t) + if e != nil { + return nil, e + } + } + return us, nil +} diff --git a/moslice/map_test.go b/moslice/map_test.go new file mode 100644 index 0000000..d15bf7d --- /dev/null +++ b/moslice/map_test.go @@ -0,0 +1,58 @@ +package moslice_test + +import ( + "errors" + "lmika.dev/pkg/modash/moslice" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMap(t *testing.T) { + t.Run("should return a mapped slice", func(t *testing.T) { + ts := []int{1, 2, 3} + us := moslice.Map(ts, func(x int) int { return x + 2 }) + + assert.Equal(t, []int{3, 4, 5}, us) + }) + + t.Run("should return nil if passed in nil", func(t *testing.T) { + ts := []int(nil) + us := moslice.Map(ts, func(x int) int { return x + 2 }) + + assert.Nil(t, us) + }) +} + +func TestMapWithError(t *testing.T) { + t.Run("should return a mapped slice with no error", func(t *testing.T) { + ts := []int{1, 2, 3} + + us, err := moslice.MapWithError(ts, func(x int) (int, error) { return x + 2, nil }) + + assert.Equal(t, []int{3, 4, 5}, us) + assert.NoError(t, err) + }) + + t.Run("should return nil with an error when mapping function returns an error", func(t *testing.T) { + ts := []int{1, 2, 3} + + us, err := moslice.MapWithError(ts, func(x int) (int, error) { + if x == 2 { + return 0, errors.New("bang") + } + return x + 2, nil + }) + + assert.Nil(t, us) + assert.Error(t, err) + }) + + t.Run("should return nil if passed in nil", func(t *testing.T) { + ts := []int(nil) + us, err := moslice.MapWithError(ts, func(x int) (int, error) { return x + 2, nil }) + + assert.Nil(t, us) + assert.NoError(t, err) + }) +} diff --git a/moslice/uniq.go b/moslice/uniq.go new file mode 100644 index 0000000..9e2f346 --- /dev/null +++ b/moslice/uniq.go @@ -0,0 +1,19 @@ +package moslice + +func Uniq[T comparable](ts []T) []T { + if len(ts) < 2 { + return ts + } + + outT := make([]T, 0) + seenT := make(map[T]struct{}) + + for _, t := range ts { + if _, ok := seenT[t]; !ok { + outT = append(outT, t) + seenT[t] = struct{}{} + } + } + + return outT +} diff --git a/moslice/uniq_test.go b/moslice/uniq_test.go new file mode 100644 index 0000000..23269ce --- /dev/null +++ b/moslice/uniq_test.go @@ -0,0 +1,31 @@ +package moslice_test + +import ( + "fmt" + "lmika.dev/pkg/modash/moslice" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUniq(t *testing.T) { + t.Run("should return a slice with unique elements", func(t *testing.T) { + scenarios := []struct { + in []int + want []int + }{ + {in: nil, want: nil}, + {in: []int{}, want: []int{}}, + {in: []int{2}, want: []int{2}}, + {in: []int{1, 2}, want: []int{1, 2}}, + {in: []int{2, 2}, want: []int{2}}, + {in: []int{3, 1, 4, 2, 3, 5, 1, 4}, want: []int{3, 1, 4, 2, 5}}, + } + + for i, s := range scenarios { + t.Run(fmt.Sprint(i), func(t *testing.T) { + assert.Equal(t, s.want, moslice.Uniq(s.in)) + }) + } + }) +}