From 9cf61ec6c5ee43727c310caad6bbea193f1a3a07 Mon Sep 17 00:00:00 2001 From: Ruidy Date: Mon, 1 Sep 2025 18:16:59 -0400 Subject: [PATCH] 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. --- README.md | 20 +++++ contains_by_test.go | 25 ------ contains_test.go | 16 ++++ docs/content/collections/parallel_filter.md | 24 ++++++ docs/content/collections/unique_in_place.md | 21 +++++ parallel_filter.go | 86 +++++++++++++++++++++ parallel_filter_test.go | 33 ++++++++ unique_in_place.go | 17 ++++ unique_in_place_test.go | 16 ++++ 9 files changed, 233 insertions(+), 25 deletions(-) delete mode 100644 contains_by_test.go create mode 100644 docs/content/collections/parallel_filter.md create mode 100644 docs/content/collections/unique_in_place.md create mode 100644 parallel_filter.go create mode 100644 parallel_filter_test.go create mode 100644 unique_in_place.go create mode 100644 unique_in_place_test.go diff --git a/README.md b/README.md index 54dc9cc..77e0b30 100644 --- a/README.md +++ b/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] +} +``` + ### Subpackages - `maps.Keys(m)` / `maps.Values(m)`: utilities to extract keys or values from maps. diff --git a/contains_by_test.go b/contains_by_test.go deleted file mode 100644 index aab05e5..0000000 --- a/contains_by_test.go +++ /dev/null @@ -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" })) -} diff --git a/contains_test.go b/contains_test.go index 90cd9e6..e8c0ef6 100644 --- a/contains_test.go +++ b/contains_test.go @@ -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" })) +} diff --git a/docs/content/collections/parallel_filter.md b/docs/content/collections/parallel_filter.md new file mode 100644 index 0000000..7f06a50 --- /dev/null +++ b/docs/content/collections/parallel_filter.md @@ -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] +} +``` diff --git a/docs/content/collections/unique_in_place.md b/docs/content/collections/unique_in_place.md new file mode 100644 index 0000000..b67224d --- /dev/null +++ b/docs/content/collections/unique_in_place.md @@ -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] +} +``` diff --git a/parallel_filter.go b/parallel_filter.go new file mode 100644 index 0000000..ef318ae --- /dev/null +++ b/parallel_filter.go @@ -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 +} diff --git a/parallel_filter_test.go b/parallel_filter_test.go new file mode 100644 index 0000000..d001440 --- /dev/null +++ b/parallel_filter_test.go @@ -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) +} diff --git a/unique_in_place.go b/unique_in_place.go new file mode 100644 index 0000000..c88cca1 --- /dev/null +++ b/unique_in_place.go @@ -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] +} diff --git a/unique_in_place_test.go b/unique_in_place_test.go new file mode 100644 index 0000000..c6a777e --- /dev/null +++ b/unique_in_place_test.go @@ -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) +}