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`
|
||||
- `Unique`
|
||||
- `UniqueBy`
|
||||
- `UniqueInPlace`
|
||||
- `Chunk`
|
||||
|
||||
### Pipe
|
||||
|
|
@ -119,6 +120,7 @@ and return `Value` instantly.
|
|||
### Concurrency
|
||||
|
||||
- `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
|
||||
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
|
||||
|
||||
- `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}
|
||||
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