mirror of
https://github.com/rjNemo/underscore
synced 2026-06-06 02:26:42 +00:00
feat: add Chunk, ContainsBy, UniqueBy, ParallelMap, map helpers
- Add `Chunk` to split slices into groups of size n. - Add `ContainsBy` for predicate-based containment checks. - Add `UniqueBy` to deduplicate slices by key selector. - Add `ParallelMap` for concurrent mapping with context and error handling. - Add `maps.Keys` and `maps.Values` helpers for extracting map keys/values. - Update README and docs for new features. - Refactor `Contains` to use `slices.Contains`.
This commit is contained in:
parent
8c78743f1a
commit
1031038d42
21 changed files with 442 additions and 17 deletions
41
README.md
41
README.md
|
|
@ -20,8 +20,8 @@ It is mostly a port from the `underscore.js` library based on generics brought b
|
||||||
|
|
||||||
Install the library using
|
Install the library using
|
||||||
|
|
||||||
```shell
|
```sh
|
||||||
go get github.com/rjNemo/underscore@0.4.0
|
go get github.com/rjNemo/underscore@0.7.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Please check out the [examples](examples) to see how to use the library.
|
Please check out the [examples](examples) to see how to use the library.
|
||||||
|
|
@ -64,13 +64,13 @@ download page](https://go.dev/dl/) and install version `1.24` or beyond.
|
||||||
|
|
||||||
First clone the repository
|
First clone the repository
|
||||||
|
|
||||||
```shell
|
```sh
|
||||||
git clone https://github.com/rjNemo/underscore.git
|
git clone https://github.com/rjNemo/underscore.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Install dependencies
|
Install dependencies
|
||||||
|
|
||||||
```shell
|
```sh
|
||||||
go mod download
|
go mod download
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ And that's it.
|
||||||
|
|
||||||
To run the unit tests, you can simply run:
|
To run the unit tests, you can simply run:
|
||||||
|
|
||||||
```shell
|
```sh
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -92,7 +92,8 @@ make test
|
||||||
|
|
||||||
- `All`
|
- `All`
|
||||||
- `Any`
|
- `Any`
|
||||||
- `Contains` (only numerics values at the moment)
|
- `Contains`
|
||||||
|
- `ContainsBy`
|
||||||
- `Each`
|
- `Each`
|
||||||
- `Filter`
|
- `Filter`
|
||||||
- `Flatmap`
|
- `Flatmap`
|
||||||
|
|
@ -103,6 +104,9 @@ make test
|
||||||
- `Min`
|
- `Min`
|
||||||
- `Partition`
|
- `Partition`
|
||||||
- `Reduce`
|
- `Reduce`
|
||||||
|
- `Unique`
|
||||||
|
- `UniqueBy`
|
||||||
|
- `Chunk`
|
||||||
|
|
||||||
### Pipe
|
### Pipe
|
||||||
|
|
||||||
|
|
@ -112,6 +116,31 @@ you've finished the computation, call `Value` to retrieve the final value.
|
||||||
Methods not returning a slice such as `Reduce`, `All`, `Any`, will break the `Chain`
|
Methods not returning a slice such as `Reduce`, `All`, `Any`, will break the `Chain`
|
||||||
and return `Value` instantly.
|
and return `Value` instantly.
|
||||||
|
|
||||||
|
### Concurrency
|
||||||
|
|
||||||
|
- `ParallelMap(ctx, values, workers, fn)`: apply a function concurrently while preserving order and supporting context cancellation.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out, err := u.ParallelMap(context.Background(), []int{1, 2, 3, 4}, 4,
|
||||||
|
func(ctx context.Context, n int) (int, error) { return n * n, nil },
|
||||||
|
)
|
||||||
|
fmt.Println(out, err) // [1 4 9 16] <nil>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subpackages
|
||||||
|
|
||||||
|
- `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps.
|
||||||
|
|
||||||
## Built With
|
## Built With
|
||||||
|
|
||||||
- [Go](https://go.dev/) - Build fast, reliable, and efficient software at scale
|
- [Go](https://go.dev/) - Build fast, reliable, and efficient software at scale
|
||||||
|
|
|
||||||
19
chunk.go
Normal file
19
chunk.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// Chunk splits the input slice into groups of size n.
|
||||||
|
// If n <= 0, it returns nil. The final chunk may be smaller than n.
|
||||||
|
func Chunk[T any](values []T, n int) [][]T {
|
||||||
|
if n <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l := len(values)
|
||||||
|
if l == 0 {
|
||||||
|
return [][]T{}
|
||||||
|
}
|
||||||
|
chunks := make([][]T, 0, (l+n-1)/n)
|
||||||
|
for i := 0; i < l; i += n {
|
||||||
|
j := min(i+n, l)
|
||||||
|
chunks = append(chunks, values[i:j])
|
||||||
|
}
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
34
chunk_test.go
Normal file
34
chunk_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChunk(t *testing.T) {
|
||||||
|
in := []int{1, 2, 3, 4, 5}
|
||||||
|
got := u.Chunk(in, 2)
|
||||||
|
want := [][]int{{1, 2}, {3, 4}, {5}}
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkLargeSize(t *testing.T) {
|
||||||
|
in := []int{1, 2, 3}
|
||||||
|
got := u.Chunk(in, 10)
|
||||||
|
want := [][]int{{1, 2, 3}}
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkInvalidSize(t *testing.T) {
|
||||||
|
var in []int
|
||||||
|
assert.Nil(t, u.Chunk(in, 0))
|
||||||
|
assert.Nil(t, u.Chunk(in, -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChunkEmpty(t *testing.T) {
|
||||||
|
got := u.Chunk([]int{}, 1)
|
||||||
|
assert.Equal(t, 0, len(got))
|
||||||
|
}
|
||||||
14
contains.go
14
contains.go
|
|
@ -1,11 +1,13 @@
|
||||||
package underscore
|
package underscore
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
// Contains returns true if the value is present in the slice
|
// Contains returns true if the value is present in the slice
|
||||||
func Contains[T comparable](values []T, value T) bool {
|
func Contains[T comparable](values []T, value T) bool {
|
||||||
for _, v := range values {
|
return slices.Contains(values, value)
|
||||||
if v == value {
|
}
|
||||||
return true
|
|
||||||
}
|
// ContainsBy returns true if any element in the slice satisfies the predicate.
|
||||||
}
|
func ContainsBy[T any](values []T, predicate func(T) bool) bool {
|
||||||
return false
|
return slices.ContainsFunc(values, predicate)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
contains_by_test.go
Normal file
25
contains_by_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContainsBy(t *testing.T) {
|
||||||
|
nums := []int{1, 3, 5, 8}
|
||||||
|
assert.True(t, u.ContainsBy(nums, func(n int) bool { return n%2 == 0 }))
|
||||||
|
assert.False(t, u.ContainsBy(nums, func(n int) bool { return n < 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainsByStruct(t *testing.T) {
|
||||||
|
type user struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
users := []user{{1, "a"}, {2, "b"}, {3, "c"}}
|
||||||
|
assert.True(t, u.ContainsBy(users, func(u user) bool { return u.ID == 2 }))
|
||||||
|
assert.False(t, u.ContainsBy(users, func(u user) bool { return u.Name == "z" }))
|
||||||
|
}
|
||||||
19
docs/content/collections/chunk.md
Normal file
19
docs/content/collections/chunk.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
title: "Chunk"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`Chunk` splits a slice into groups of size `n`. The last chunk may be smaller.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println(u.Chunk([]int{1,2,3,4,5}, 2)) // [[1 2] [3 4] [5]]
|
||||||
|
}
|
||||||
|
```
|
||||||
20
docs/content/collections/containsby.md
Normal file
20
docs/content/collections/containsby.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
title: "ContainsBy"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`ContainsBy` returns true if any element satisfies the predicate.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
nums := []int{1, 3, 5, 8}
|
||||||
|
fmt.Println(u.ContainsBy(nums, func(n int) bool { return n%2 == 0 })) // true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -3,7 +3,8 @@ title: "Map"
|
||||||
date: 2022-03-21T13:32:10-04:00
|
date: 2022-03-21T13:32:10-04:00
|
||||||
---
|
---
|
||||||
|
|
||||||
`Map` produces a new slice of values by mapping each value in the slice through a transform function.
|
`Map` produces a new slice of values by mapping each value in the slice through a
|
||||||
|
transform function.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
|
||||||
25
docs/content/collections/parallel_map.md
Normal file
25
docs/content/collections/parallel_map.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: "ParallelMap"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`ParallelMap` applies a function to each element concurrently using a worker pool,
|
||||||
|
preserves order, and supports context cancellation.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out, err := u.ParallelMap(context.Background(),
|
||||||
|
[]int{1,2,3,4}, 4, func(ctx context.Context, n int) (int, error) {
|
||||||
|
return n*n, nil
|
||||||
|
})
|
||||||
|
fmt.Println(out, err) // [1 4 9 16] <nil>
|
||||||
|
}
|
||||||
|
```
|
||||||
24
docs/content/collections/unique_by.md
Normal file
24
docs/content/collections/unique_by.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: "UniqueBy"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`UniqueBy` returns a duplicate-free version of the slice using a key selector.
|
||||||
|
Order is preserved; the first occurrence of each key is kept.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct{ ID int; Email string }
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
users := []User{{1, "a@x"}, {2, "b@x"}, {3, "a@x"}}
|
||||||
|
fmt.Println(u.UniqueBy(users, func(u User) string { return u.Email }))
|
||||||
|
// [{1 a@x} {2 b@x}]
|
||||||
|
}
|
||||||
|
```
|
||||||
6
docs/content/maps/_index.md
Normal file
6
docs/content/maps/_index.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: "Map Helpers"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
Utilities for Go maps provided by the `maps` subpackage.
|
||||||
19
docs/content/maps/keys.md
Normal file
19
docs/content/maps/keys.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
title: "Keys"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`maps.Keys` returns the keys of a map in unspecified order.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
m "github.com/rjNemo/underscore/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println(m.Keys(map[int]string{1:"a",2:"b"})) // e.g., [2 1]
|
||||||
|
}
|
||||||
|
```
|
||||||
19
docs/content/maps/values.md
Normal file
19
docs/content/maps/values.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
title: "Values"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`maps.Values` returns the values of a map in unspecified order.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
m "github.com/rjNemo/underscore/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println(m.Values(map[int]string{1:"a",2:"b"})) // e.g., ["b" "a"]
|
||||||
|
}
|
||||||
|
```
|
||||||
19
maps/keys_values.go
Normal file
19
maps/keys_values.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package maps
|
||||||
|
|
||||||
|
// Keys returns the keys of the provided map in unspecified order.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the values of the provided map in unspecified order.
|
||||||
|
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
|
||||||
|
}
|
||||||
22
maps/keys_values_test.go
Normal file
22
maps/keys_values_test.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package maps_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
m "github.com/rjNemo/underscore/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeysValues(t *testing.T) {
|
||||||
|
in := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||||
|
ks := m.Keys(in)
|
||||||
|
vs := m.Values(in)
|
||||||
|
|
||||||
|
// Order is unspecified; verify content and lengths.
|
||||||
|
assert.Len(t, ks, 3)
|
||||||
|
assert.ElementsMatch(t, []int{1, 2, 3}, ks)
|
||||||
|
|
||||||
|
assert.Len(t, vs, 3)
|
||||||
|
assert.ElementsMatch(t, []string{"a", "b", "c"}, vs)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package maps
|
package maps
|
||||||
|
|
||||||
|
import "maps"
|
||||||
|
|
||||||
type M[K comparable, V any] map[K]V
|
type M[K comparable, V any] map[K]V
|
||||||
|
|
||||||
// Map produces a new slice of values by mapping each value in the slice through
|
// Map produces a new slice of values by mapping each value in the slice through
|
||||||
|
|
@ -8,9 +10,7 @@ func Map[K, Q comparable, V, W any](m M[K, V], f func(K, V) M[Q, W]) M[Q, W] {
|
||||||
res := make(M[Q, W], len(m))
|
res := make(M[Q, W], len(m))
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
mm := f(k, v)
|
mm := f(k, v)
|
||||||
for k2, v2 := range mm {
|
maps.Copy(res, mm)
|
||||||
res[k2] = v2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ func TestMap(t *testing.T) {
|
||||||
"alice": false,
|
"alice": false,
|
||||||
"bob": false,
|
"bob": false,
|
||||||
"clara": false,
|
"clara": false,
|
||||||
"david": true}
|
"david": true,
|
||||||
|
}
|
||||||
assert.Equal(t, want, m.Map(scores, hasWon))
|
assert.Equal(t, want, m.Map(scores, hasWon))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
72
parallel_map.go
Normal file
72
parallel_map.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParallelMap applies fn to each element of values using a worker pool and preserves order.
|
||||||
|
// If workers <= 0, it defaults to GOMAXPROCS.
|
||||||
|
// On error, the first error is returned and processing is canceled; partial results are discarded.
|
||||||
|
func ParallelMap[T, P any](ctx context.Context, values []T, workers int, fn func(context.Context, T) (P, error)) ([]P, error) {
|
||||||
|
if workers <= 0 {
|
||||||
|
workers = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
type task struct {
|
||||||
|
idx int
|
||||||
|
val T
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]P, len(values))
|
||||||
|
tasks := make(chan task)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var once sync.Once
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
worker := func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for t := range tasks {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
v, err := fn(ctx, t.val)
|
||||||
|
if err != nil {
|
||||||
|
once.Do(func() {
|
||||||
|
firstErr = err
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res[t.idx] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(workers)
|
||||||
|
for i := 0; i < workers; i++ {
|
||||||
|
go worker()
|
||||||
|
}
|
||||||
|
|
||||||
|
OUTER:
|
||||||
|
for i, v := range values {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
break OUTER
|
||||||
|
default:
|
||||||
|
tasks <- task{idx: i, val: v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(tasks)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if firstErr != nil {
|
||||||
|
return nil, firstErr
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
33
parallel_map_test.go
Normal file
33
parallel_map_test.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParallelMap_OrderAndResult(t *testing.T) {
|
||||||
|
values := []int{1, 2, 3, 4, 5}
|
||||||
|
out, err := u.ParallelMap(context.Background(), values, 2, func(_ context.Context, n int) (int, error) {
|
||||||
|
return n * n, nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []int{1, 4, 9, 16, 25}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelMap_Error(t *testing.T) {
|
||||||
|
values := []int{1, 2, 3, 4, 5}
|
||||||
|
wantErr := errors.New("boom")
|
||||||
|
out, err := u.ParallelMap(context.Background(), values, 4, func(_ context.Context, n int) (int, error) {
|
||||||
|
if n == 3 {
|
||||||
|
return 0, wantErr
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
16
unique_by.go
Normal file
16
unique_by.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// UniqueBy returns a slice of unique values from the given slice using a key selector.
|
||||||
|
// The first occurrence of each key is kept and order is preserved.
|
||||||
|
func UniqueBy[T any, K comparable](values []T, key func(T) K) (uniques []T) {
|
||||||
|
seen := make(map[K]struct{})
|
||||||
|
for _, v := range values {
|
||||||
|
k := key(v)
|
||||||
|
if _, ok := seen[k]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[k] = struct{}{}
|
||||||
|
uniques = append(uniques, v)
|
||||||
|
}
|
||||||
|
return uniques
|
||||||
|
}
|
||||||
20
unique_by_test.go
Normal file
20
unique_by_test.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUniqueBy(t *testing.T) {
|
||||||
|
type user struct {
|
||||||
|
ID int
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
in := []user{{1, "a@x"}, {2, "b@x"}, {3, "a@x"}, {4, "c@x"}, {5, "b@x"}}
|
||||||
|
out := u.UniqueBy(in, func(u user) string { return u.Email })
|
||||||
|
want := []user{{1, "a@x"}, {2, "b@x"}, {4, "c@x"}}
|
||||||
|
assert.Equal(t, want, out)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue