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:
Ruidy 2025-09-01 18:16:59 -04:00
parent 1031038d42
commit 9cf61ec6c5
No known key found for this signature in database
GPG key ID: 705C24D202990805
9 changed files with 233 additions and 25 deletions

View file

@ -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.

View file

@ -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" }))
}

View file

@ -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" }))
}

View 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>
}
```

View 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
View 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
View 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
View 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
View 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)
}