mirror of
https://github.com/rjNemo/underscore
synced 2026-06-06 02:26:42 +00:00
feat: add ParallelFilter and UniqueInPlace functions
Add `ParallelFilter` for concurrent filtering with context and error support. Add `UniqueInPlace` to remove duplicates from slices in place. Update README and add documentation and tests for both functions.
This commit is contained in:
parent
1031038d42
commit
9cf61ec6c5
9 changed files with 233 additions and 25 deletions
20
README.md
20
README.md
|
|
@ -106,6 +106,7 @@ make test
|
||||||
- `Reduce`
|
- `Reduce`
|
||||||
- `Unique`
|
- `Unique`
|
||||||
- `UniqueBy`
|
- `UniqueBy`
|
||||||
|
- `UniqueInPlace`
|
||||||
- `Chunk`
|
- `Chunk`
|
||||||
|
|
||||||
### Pipe
|
### Pipe
|
||||||
|
|
@ -119,6 +120,7 @@ and return `Value` instantly.
|
||||||
### Concurrency
|
### Concurrency
|
||||||
|
|
||||||
- `ParallelMap(ctx, values, workers, fn)`: apply a function concurrently while preserving order and supporting context cancellation.
|
- `ParallelMap(ctx, values, workers, fn)`: apply a function concurrently while preserving order and supporting context cancellation.
|
||||||
|
- `ParallelFilter(ctx, values, workers, fn)`: filter concurrently with order preserved and context support.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
@ -137,6 +139,24 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ParallelFilter example
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out, err := u.ParallelFilter(context.Background(), []int{1,2,3,4,5}, 3,
|
||||||
|
func(ctx context.Context, n int) (bool, error) { return n%2==0, nil },
|
||||||
|
)
|
||||||
|
fmt.Println(out, err) // [2 4] <nil>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Subpackages
|
### Subpackages
|
||||||
|
|
||||||
- `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps.
|
- `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps.
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
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" }))
|
|
||||||
}
|
|
||||||
|
|
@ -17,3 +17,19 @@ func TestNotContains(t *testing.T) {
|
||||||
nums := []int{1, 3, 5, 7, 9}
|
nums := []int{1, 3, 5, 7, 9}
|
||||||
assert.False(t, u.Contains(nums, 15))
|
assert.False(t, u.Contains(nums, 15))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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" }))
|
||||||
|
}
|
||||||
|
|
|
||||||
24
docs/content/collections/parallel_filter.md
Normal file
24
docs/content/collections/parallel_filter.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
title: "ParallelFilter"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`ParallelFilter` filters a slice concurrently with 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.ParallelFilter(context.Background(), []int{1,2,3,4,5}, 3,
|
||||||
|
func(ctx context.Context, n int) (bool, error) { return n%2==0, nil },
|
||||||
|
)
|
||||||
|
fmt.Println(out, err) // [2 4] <nil>
|
||||||
|
}
|
||||||
|
```
|
||||||
21
docs/content/collections/unique_in_place.md
Normal file
21
docs/content/collections/unique_in_place.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
---
|
||||||
|
title: "UniqueInPlace"
|
||||||
|
date: 2025-09-01T00:00:00-00:00
|
||||||
|
---
|
||||||
|
|
||||||
|
`UniqueInPlace` removes duplicates from a slice in place while preserving order.
|
||||||
|
Returns the shortened slice.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
xs := []int{1,4,2,5,3,1,5,2}
|
||||||
|
fmt.Println(u.UniqueInPlace(xs)) // [1 4 2 5 3]
|
||||||
|
}
|
||||||
|
```
|
||||||
86
parallel_filter.go
Normal file
86
parallel_filter.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParallelFilter filters values using a context-aware predicate concurrently and preserves input order.
|
||||||
|
// If workers <= 0, it defaults to GOMAXPROCS. On error, cancels work and returns nil with the error.
|
||||||
|
func ParallelFilter[T any](ctx context.Context, values []T, workers int, fn func(context.Context, T) (bool, error)) ([]T, error) {
|
||||||
|
if workers <= 0 {
|
||||||
|
workers = runtime.GOMAXPROCS(0)
|
||||||
|
}
|
||||||
|
type task struct {
|
||||||
|
idx int
|
||||||
|
val T
|
||||||
|
}
|
||||||
|
|
||||||
|
keeps := make([]bool, 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:
|
||||||
|
}
|
||||||
|
keep, err := fn(ctx, t.val)
|
||||||
|
if err != nil {
|
||||||
|
once.Do(func() {
|
||||||
|
firstErr = err
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keeps[t.idx] = keep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result preserving order
|
||||||
|
// Pre-count capacity to avoid re-allocations
|
||||||
|
count := 0
|
||||||
|
for _, k := range keeps {
|
||||||
|
if k {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res := make([]T, 0, count)
|
||||||
|
for i, k := range keeps {
|
||||||
|
if k {
|
||||||
|
res = append(res, values[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
33
parallel_filter_test.go
Normal file
33
parallel_filter_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 TestParallelFilter_OrderAndResult(t *testing.T) {
|
||||||
|
values := []int{1, 2, 3, 4, 5}
|
||||||
|
out, err := u.ParallelFilter(context.Background(), values, 3, func(_ context.Context, n int) (bool, error) {
|
||||||
|
return n%2 == 0, nil
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, []int{2, 4}, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParallelFilter_Error(t *testing.T) {
|
||||||
|
values := []int{1, 2, 3, 4, 5}
|
||||||
|
boom := errors.New("boom")
|
||||||
|
out, err := u.ParallelFilter(context.Background(), values, 2, func(_ context.Context, n int) (bool, error) {
|
||||||
|
if n == 4 {
|
||||||
|
return false, boom
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, out)
|
||||||
|
}
|
||||||
17
unique_in_place.go
Normal file
17
unique_in_place.go
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
package underscore
|
||||||
|
|
||||||
|
// UniqueInPlace removes duplicate elements from the slice in place, preserving order.
|
||||||
|
// It returns the shortened slice containing the first occurrence of each value.
|
||||||
|
func UniqueInPlace[T comparable](values []T) []T {
|
||||||
|
seen := make(map[T]struct{}, len(values))
|
||||||
|
w := 0
|
||||||
|
for _, v := range values {
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
values[w] = v
|
||||||
|
w++
|
||||||
|
}
|
||||||
|
return values[:w]
|
||||||
|
}
|
||||||
16
unique_in_place_test.go
Normal file
16
unique_in_place_test.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package underscore_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
u "github.com/rjNemo/underscore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUniqueInPlace(t *testing.T) {
|
||||||
|
nums := []int{1, 4, 2, 5, 3, 1, 5, 2, 8, 9}
|
||||||
|
got := u.UniqueInPlace(nums)
|
||||||
|
want := []int{1, 4, 2, 5, 3, 8, 9}
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue