From ff0e293ce3dbde1e80a1b1eb059078aa7d1442c4 Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Sun, 1 Mar 2026 20:43:51 +0100 Subject: [PATCH] feat: adding FilterErr helpers --- README.md | 11 +++++ docs/data/core-filter.md | 1 + docs/data/core-filtererr.md | 37 ++++++++++++++ lo_example_test.go | 20 ++++++++ slice.go | 19 +++++++ slice_test.go | 99 +++++++++++++++++++++++++++++++++++++ 6 files changed, 187 insertions(+) create mode 100644 docs/data/core-filtererr.md diff --git a/README.md b/README.md index 62adb0b..2df8825 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,17 @@ even := lo.Filter([]int{1, 2, 3, 4}, func(x int, index int) bool { // []int{2, 4} ``` +```go +// Use FilterErr when the predicate can return an error +even, err := lo.FilterErr([]int{1, 2, 3, 4}, func(x int, _ int) (bool, error) { + if x == 3 { + return false, fmt.Errorf("number 3 is not allowed") + } + return x%2 == 0, nil +}) +// []int(nil), error("number 3 is not allowed") +``` + [[play](https://go.dev/play/p/Apjg3WeSi7K)] Mutable: like `lo.Filter()`, but the slice is updated in place. diff --git a/docs/data/core-filter.md b/docs/data/core-filter.md index 554b2f0..0c0d409 100644 --- a/docs/data/core-filter.md +++ b/docs/data/core-filter.md @@ -7,6 +7,7 @@ subCategory: slice playUrl: https://go.dev/play/p/Apjg3WeSi7K similarHelpers: - core#slice#reject + - core#slice#filtererr - core#slice#filtermap - core#slice#filterreject - core#slice#rejectmap diff --git a/docs/data/core-filtererr.md b/docs/data/core-filtererr.md new file mode 100644 index 0000000..bd54170 --- /dev/null +++ b/docs/data/core-filtererr.md @@ -0,0 +1,37 @@ +--- +name: FilterErr +slug: filtererr +sourceRef: slice.go#L27 +category: core +subCategory: slice +signatures: + - "func FilterErr[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) (bool, error)) (Slice, error)" +playUrl: +variantHelpers: + - core#slice#filtererr +similarHelpers: + - core#slice#filter + - core#slice#reject + - core#slice#filtermap + - core#slice#filterreject +position: 5 +--- + +Iterates over a collection and returns a slice of all the elements the predicate function returns `true` for. If the predicate returns an error, iteration stops immediately and returns the error. + +```go +even, err := lo.FilterErr([]int{1, 2, 3, 4}, func(x int, index int) (bool, error) { + if x == 3 { + return false, errors.New("number 3 is not allowed") + } + return x%2 == 0, nil +}) +// []int(nil), error("number 3 is not allowed") +``` + +```go +even, err := lo.FilterErr([]int{1, 2, 3, 4}, func(x int, index int) (bool, error) { + return x%2 == 0, nil +}) +// []int{2, 4}, nil +``` diff --git a/lo_example_test.go b/lo_example_test.go index aa96c46..fb0fdfc 100644 --- a/lo_example_test.go +++ b/lo_example_test.go @@ -2354,6 +2354,26 @@ func ExampleFilter() { // Output: [2 4] } +func ExampleFilterErr() { + list := []int64{1, 2, 3, 4} + + result, err := FilterErr(list, func(nbr int64, index int) (bool, error) { + if nbr == 3 { + return false, fmt.Errorf("number 3 is not allowed") + } + return nbr%2 == 0, nil + }) + fmt.Printf("%v, %v\n", result, err) + + result, err = FilterErr([]int64{1, 2, 4, 6}, func(nbr int64, index int) (bool, error) { + return nbr%2 == 0, nil + }) + fmt.Printf("%v, %v\n", result, err) + // Output: + // [], number 3 is not allowed + // [2 4 6], +} + func ExampleMap() { list := []int64{1, 2, 3, 4} diff --git a/slice.go b/slice.go index 3165dcc..ec61f3c 100644 --- a/slice.go +++ b/slice.go @@ -21,6 +21,25 @@ func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T, index in return result } +// FilterErr iterates over elements of collection, returning a slice of all elements predicate returns true for. +// If the predicate returns an error, iteration stops immediately and returns the error. +// Play: https://go.dev/play/p/Apjg3WeSi7K +func FilterErr[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) (bool, error)) (Slice, error) { + result := make(Slice, 0, len(collection)) + + for i := range collection { + ok, err := predicate(collection[i], i) + if err != nil { + return nil, err + } + if ok { + result = append(result, collection[i]) + } + } + + return result, nil +} + // Map manipulates a slice and transforms it to a slice of another type. // Play: https://go.dev/play/p/OkPcYAhBo0D func Map[T, R any](collection []T, transform func(item T, index int) R) []R { diff --git a/slice_test.go b/slice_test.go index 013719b..7f1138b 100644 --- a/slice_test.go +++ b/slice_test.go @@ -34,6 +34,105 @@ func TestFilter(t *testing.T) { is.IsType(nonempty, allStrings, "type preserved") } +func TestFilterErr(t *testing.T) { + t.Parallel() + is := assert.New(t) + + tests := []struct { + name string + input []int + predicate func(item int, index int) (bool, error) + want []int + wantErr string + callbacks int // Number of predicates called before error/finish + }{ + { + name: "filter even numbers", + input: []int{1, 2, 3, 4}, + predicate: func(x int, _ int) (bool, error) { + return x%2 == 0, nil + }, + want: []int{2, 4}, + callbacks: 4, + }, + { + name: "empty slice", + input: []int{}, + predicate: func(x int, _ int) (bool, error) { + return true, nil + }, + want: []int{}, + callbacks: 0, + }, + { + name: "filter all out", + input: []int{1, 2, 3, 4}, + predicate: func(x int, _ int) (bool, error) { + return false, nil + }, + want: []int{}, + callbacks: 4, + }, + { + name: "filter all in", + input: []int{1, 2, 3, 4}, + predicate: func(x int, _ int) (bool, error) { + return true, nil + }, + want: []int{1, 2, 3, 4}, + callbacks: 4, + }, + { + name: "error on specific index", + input: []int{1, 2, 3, 4}, + predicate: func(x int, _ int) (bool, error) { + if x == 3 { + return false, fmt.Errorf("number 3 is not allowed") + } + return x%2 == 0, nil + }, + callbacks: 3, + wantErr: "number 3 is not allowed", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var callbacks int + wrappedPredicate := func(item int, index int) (bool, error) { + callbacks++ + return tt.predicate(item, index) + } + + got, err := FilterErr(tt.input, wrappedPredicate) + + if tt.wantErr != "" { + is.Error(err) + is.Equal(tt.wantErr, err.Error()) + is.Nil(got) + is.Equal(tt.callbacks, callbacks, "callback count should match expected early return") + } else { + is.NoError(err) + is.Equal(tt.want, got) + is.Equal(tt.callbacks, callbacks) + } + }) + } + + // Test type preservation + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty, err := FilterErr(allStrings, func(x string, _ int) (bool, error) { + return len(x) > 0, nil + }) + is.NoError(err) + is.IsType(nonempty, allStrings, "type preserved") + is.Equal(myStrings{"foo", "bar"}, nonempty) +} + func TestMap(t *testing.T) { t.Parallel() is := assert.New(t)