From 0d97c86fed54d1209cc2b07d482da08d4e08de9f Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Mon, 17 Nov 2025 17:26:38 +0000 Subject: [PATCH 1/4] :sparkles: `[collection]` Added execute a function on each element of a function --- changes/20251117162958.feature | 1 + changes/20251117163045.feature | 1 + changes/20251117163100.feature | 1 + utils/collection/conditions.go | 46 +++++-- utils/collection/conditions_test.go | 8 ++ utils/collection/search.go | 178 +++++++++++++++++++++++----- utils/collection/search_test.go | 101 +++++++++++++++- 7 files changed, 294 insertions(+), 42 deletions(-) create mode 100644 changes/20251117162958.feature create mode 100644 changes/20251117163045.feature create mode 100644 changes/20251117163100.feature diff --git a/changes/20251117162958.feature b/changes/20251117162958.feature new file mode 100644 index 0000000000..a5ca585a4a --- /dev/null +++ b/changes/20251117162958.feature @@ -0,0 +1 @@ +:sparkles: `[collection]` Added execute a function on each element of a function diff --git a/changes/20251117163045.feature b/changes/20251117163045.feature new file mode 100644 index 0000000000..a130a634a1 --- /dev/null +++ b/changes/20251117163045.feature @@ -0,0 +1 @@ +:zap: `[collection]` Support `iter.Seq` for most operations on slices (map, filter) to increase performance diff --git a/changes/20251117163100.feature b/changes/20251117163100.feature new file mode 100644 index 0000000000..1fb35cecc0 --- /dev/null +++ b/changes/20251117163100.feature @@ -0,0 +1 @@ +:sparkles: `[collection]` Added set operations on slices diff --git a/utils/collection/conditions.go b/utils/collection/conditions.go index 1f4d3741f1..3ce9f351e0 100644 --- a/utils/collection/conditions.go +++ b/utils/collection/conditions.go @@ -1,6 +1,9 @@ package collection import ( + "iter" + "slices" + "github.com/ARM-software/golang-utils/utils/commonerrors" ) @@ -120,11 +123,16 @@ func (c *Conditions) OneHot() bool { // Any returns true if there is at least one element of the slice which is true. func Any(slice []bool) bool { - if len(slice) == 0 { + return AnySequence(slices.Values(slice)) +} + +// AnySequence returns true if there is at least one element of the slice which is true. +func AnySequence(seq iter.Seq[bool]) bool { + if seq == nil { return false } - for i := range slice { - if slice[i] { + for e := range seq { + if e { return true } } @@ -136,17 +144,31 @@ func AnyTrue(values ...bool) bool { return Any(values) } -// All returns true if all items of the slice are true. -func All(slice []bool) bool { - if len(slice) == 0 { - return false - } - for i := range slice { - if !slice[i] { - return false +// AnyFalseSequence returns true if there is at least one element of the sequence which is false. +func AnyFalseSequence(eq iter.Seq[bool]) bool { + hasElements := false + for e := range eq { + hasElements = true + if !e { + return true } } - return true + return !hasElements +} + +// AnyFalse returns whether there is a value set to false +func AnyFalse(values ...bool) bool { + return AnyFalseSequence(slices.Values(values)) +} + +// AllSequence returns true if all items of the sequence are true. +func AllSequence(seq iter.Seq[bool]) bool { + return !AnyFalseSequence(seq) +} + +// All returns true if all items of the slice are true. +func All(slice []bool) bool { + return AllSequence(slices.Values(slice)) } // AllTrue returns whether all values are true. diff --git a/utils/collection/conditions_test.go b/utils/collection/conditions_test.go index 9003951fbd..10f9531542 100644 --- a/utils/collection/conditions_test.go +++ b/utils/collection/conditions_test.go @@ -25,6 +25,14 @@ func TestAnyTrue(t *testing.T) { assert.True(t, AnyTrue(true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true)) } +func TestAnyFalse(t *testing.T) { + assert.True(t, AnyFalse()) + assert.True(t, AnyFalse(false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false)) + assert.True(t, AnyFalse(false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false)) + assert.False(t, AnyFalse(true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true)) + assert.True(t, AnyFalse(true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, true, true, true, true)) +} + func TestAll(t *testing.T) { assert.False(t, All([]bool{})) assert.False(t, All([]bool{false, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false})) diff --git a/utils/collection/search.go b/utils/collection/search.go index 50ad8ad603..32c4e4178d 100644 --- a/utils/collection/search.go +++ b/utils/collection/search.go @@ -5,10 +5,14 @@ package collection import ( + "iter" "slices" "strings" + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/safecast" mapset "github.com/deckarep/golang-set/v2" + "go.uber.org/atomic" ) // Find looks for an element in a slice. If found it will @@ -20,6 +24,22 @@ func Find(slice *[]string, val string) (int, bool) { return FindInSlice(true, *slice, val) } +// FindInSequence searches a collection for an element satisfying the predicate. +func FindInSequence[E any](elements iter.Seq[E], predicate Predicate[E]) (int, bool) { + if elements == nil { + return -1, false + } + idx := atomic.NewUint64(0) + for e := range elements { + if predicate(e) { + return safecast.ToInt(idx.Load()), true + } + idx.Inc() + } + + return -1, false +} + // FindInSlice finds if any values val are present in the slice and if so returns the first index. // if strict, it checks for an exact match; otherwise it discards whitespaces and case. func FindInSlice(strict bool, slice []string, val ...string) (int, bool) { @@ -62,6 +82,46 @@ func UniqueEntries[T comparable](slice []T) []T { return subSet.ToSlice() } +// Unique returns all the unique values contained in a sequence. +func Unique[T comparable](s iter.Seq[T]) []T { + return UniqueEntries(slices.Collect(s)) +} + +// Union returns the union of two slices (only unique values are returned). +func Union[T comparable](slice1, slice2 []T) []T { + subSet := mapset.NewSet[T]() + _ = subSet.Append(slice1...) + _ = subSet.Append(slice2...) + return subSet.ToSlice() +} + +// Intersection returns the intersection of two slices (only unique values are returned). +func Intersection[T comparable](slice1, slice2 []T) []T { + subSet1 := mapset.NewSet[T]() + subSet2 := mapset.NewSet[T]() + _ = subSet1.Append(slice1...) + _ = subSet2.Append(slice2...) + return subSet1.Intersect(subSet2).ToSlice() +} + +// Difference returns the Difference between slice1 and slice2 (only unique values are returned). +func Difference[T comparable](slice1, slice2 []T) []T { + subSet1 := mapset.NewSet[T]() + subSet2 := mapset.NewSet[T]() + _ = subSet1.Append(slice1...) + _ = subSet2.Append(slice2...) + return subSet1.Difference(subSet2).ToSlice() +} + +// SymmetricDifference returns the symmetric difference between slice1 and slice2 (only unique values are returned). +func SymmetricDifference[T comparable](slice1, slice2 []T) []T { + subSet1 := mapset.NewSet[T]() + subSet2 := mapset.NewSet[T]() + _ = subSet1.Append(slice1...) + _ = subSet2.Append(slice2...) + return subSet1.SymmetricDifference(subSet2).ToSlice() +} + // AnyFunc returns whether there is at least one element of slice s for which f() returns true. func AnyFunc[S ~[]E, E any](s S, f func(E) bool) bool { conditions := NewConditions(len(s)) @@ -73,17 +133,50 @@ func AnyFunc[S ~[]E, E any](s S, f func(E) bool) bool { type FilterFunc[E any] func(E) bool +type Predicate[E any] = FilterFunc[E] + // Filter returns a new slice that contains elements from the input slice which return true when they’re passed as a parameter to the provided filtering function f. -func Filter[S ~[]E, E any](s S, f FilterFunc[E]) (result S) { - result = make(S, 0, len(s)) +func Filter[S ~[]E, E any](s S, f FilterFunc[E]) S { + return slices.Collect[E](FilterSequence[E](slices.Values(s), f)) +} - for i := range s { - if f(s[i]) { - result = append(result, s[i]) +// FilterSequence returns a new sequence that contains elements from the input sequence which return true when they’re passed as a parameter to the provided filtering function f. +func FilterSequence[E any](s iter.Seq[E], f Predicate[E]) (result iter.Seq[E]) { + return func(yield func(E) bool) { + for v := range s { + if f(v) { + if !yield(v) { + return + } + } } } +} + +// ForEachValues iterates over values and executes the passed function on each of them. +func ForEachValues[E any](f func(E), values ...E) { + ForEach(values, f) +} + +// ForEach iterates over elements and executes the passed function on each element. +func ForEach[S ~[]E, E any](s S, f func(E)) { + _ = Each[E](slices.Values(s), func(e E) error { + f(e) + return nil + }) +} - return result +// Each iterates over a sequence and executes the passed function against each element. +// If passed func returns an error, the iteration stops and the error is returned, unless it is EOF. +func Each[T any](s iter.Seq[T], f func(T) error) error { + for e := range s { + err := f(e) + if err != nil { + err = commonerrors.Ignore(err, commonerrors.ErrEOF) + return err + } + } + return nil } type MapFunc[T1, T2 any] func(T1) T2 @@ -95,15 +188,28 @@ func IdentityMapFunc[T any]() MapFunc[T, T] { } } -// Map creates a new slice and populates it with the results of calling the provided function on every element in input slice. -func Map[T1 any, T2 any](s []T1, f MapFunc[T1, T2]) (result []T2) { - result = make([]T2, len(s)) +// MapSequence creates a new sequences and populates it with the results of calling the provided function on every element of the input sequence. +func MapSequence[T1 any, T2 any](s iter.Seq[T1], f MapFunc[T1, T2]) iter.Seq[T2] { + return MapSequenceWithError[T1, T2](s, func(t1 T1) (T2, error) { + return f(t1), nil + }) +} - for i := range s { - result[i] = f(s[i]) +// MapSequenceWithError creates a new sequences and populates it with the results of calling the provided function on every element of the input sequence. If an error happens, the mapping stops. +func MapSequenceWithError[T1 any, T2 any](s iter.Seq[T1], f MapWithErrorFunc[T1, T2]) iter.Seq[T2] { + return func(yield func(T2) bool) { + for v := range s { + mapped, subErr := f(v) + if subErr != nil || !yield(mapped) { + return + } + } } +} - return result +// Map creates a new slice and populates it with the results of calling the provided function on every element in input slice. +func Map[T1 any, T2 any](s []T1, f MapFunc[T1, T2]) []T2 { + return slices.Collect[T2](MapSequence[T1, T2](slices.Values(s), f)) } // MapWithError creates a new slice and populates it with the results of calling the provided function on every element in input slice. If an error happens, the mapping stops and the error returned. @@ -122,10 +228,32 @@ func MapWithError[T1 any, T2 any](s []T1, f MapWithErrorFunc[T1, T2]) (result [] return } +// OppositeFunc returns the opposite of a FilterFunc. +func OppositeFunc[E any](f FilterFunc[E]) FilterFunc[E] { return func(e E) bool { return !f(e) } } + // Reject is the opposite of Filter and returns the elements of collection for which the filtering function f returns false. // This is functionally equivalent to slices.DeleteFunc but it returns a new slice. func Reject[S ~[]E, E any](s S, f FilterFunc[E]) S { - return Filter(s, func(e E) bool { return !f(e) }) + return Filter(s, OppositeFunc[E](f)) +} + +// RejectSequence is the opposite of FilterSequence and returns the elements of collection for which the filtering function f returns false. +func RejectSequence[E any](s iter.Seq[E], f FilterFunc[E]) iter.Seq[E] { + return FilterSequence(s, OppositeFunc[E](f)) +} + +// Reduce runs a reducer function f over all elements in the array, in ascending-index order, and accumulates them into a single value. +func Reduce[T1, T2 any](s []T1, accumulator T2, f ReduceFunc[T1, T2]) T2 { + return ReducesSequence[T1, T2](slices.Values(s), accumulator, f) +} + +// ReducesSequence runs a reducer function f over all elements of a sequence, in ascending-index order, and accumulates them into a single value. +func ReducesSequence[T1, T2 any](s iter.Seq[T1], accumulator T2, f ReduceFunc[T1, T2]) (result T2) { + result = accumulator + for e := range s { + result = f(result, e) + } + return } func match[E any](e E, matches []FilterFunc[E]) *Conditions { @@ -148,13 +276,14 @@ func MatchAll[E any](e E, matches ...FilterFunc[E]) bool { type ReduceFunc[T1, T2 any] func(T2, T1) T2 -// Reduce runs a reducer function f over all elements in the array, in ascending-index order, and accumulates them into a single value. -func Reduce[T1, T2 any](s []T1, accumulator T2, f ReduceFunc[T1, T2]) (result T2) { - result = accumulator - for i := range s { - result = f(result, s[i]) - } - return +// AllFunc returns whether f returns true for all the elements of slice s. +func AllFunc[S ~[]E, E any](s S, f func(E) bool) bool { + return AllTrueSequence(slices.Values(s), f) +} + +// AllTrueSequence returns whether f returns true for all the elements in a sequence. +func AllTrueSequence[E any](s iter.Seq[E], f func(E) bool) bool { + return AllSequence(MapSequence[E, bool](s, f)) } // AnyEmpty returns whether there is one entry in the slice which is empty. @@ -164,15 +293,6 @@ func AnyEmpty(strict bool, slice []string) bool { return found } -// AllFunc returns whether f returns true for all the elements of slice s. -func AllFunc[S ~[]E, E any](s S, f func(E) bool) bool { - conditions := NewConditions(len(s)) - for i := range s { - conditions.Add(f(s[i])) - } - return conditions.All() -} - // AllNotEmpty returns whether all elements of the slice are not empty. // If strict, then whitespaces are considered as empty strings func AllNotEmpty(strict bool, slice []string) bool { diff --git a/utils/collection/search_test.go b/utils/collection/search_test.go index be2f9f63f0..e3a5bd41a9 100644 --- a/utils/collection/search_test.go +++ b/utils/collection/search_test.go @@ -6,9 +6,13 @@ package collection import ( "fmt" + "slices" "strconv" "testing" + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" + "github.com/ARM-software/golang-utils/utils/field" "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,6 +33,32 @@ func TestFind(t *testing.T) { assert.Equal(t, 2, index) } +func TestFindInSequence(t *testing.T) { + + index, found := FindInSequence[string](slices.Values([]string{}), func(e string) bool { + return e == "D" + }) + assert.False(t, found) + assert.Equal(t, -1, index) + index, found = FindInSequence[string](slices.Values([]string{}), func(_ string) bool { + return true + }) + assert.False(t, found) + assert.Equal(t, -1, index) + + index, found = FindInSequence[string](slices.Values([]string{"A", "b", "c"}), func(e string) bool { + return e == "D" + }) + assert.False(t, found) + assert.Equal(t, -1, index) + + index, found = FindInSequence[string](slices.Values([]string{"A", "B", "b", "c"}), func(e string) bool { + return e == "b" + }) + assert.True(t, found) + assert.Equal(t, 2, index) +} + func TestFindInSlice(t *testing.T) { index, found := FindInSlice(true, nil, "D") assert.False(t, found) @@ -70,10 +100,42 @@ func TestUniqueEntries(t *testing.T) { _, found = FindInSlice(true, values, "test12") assert.True(t, found) - intValues := UniqueEntries([]int{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4}) + entry := []int{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4} + intValues := UniqueEntries(entry) + assert.Len(t, intValues, 4) + + intValues = Unique(slices.Values(entry)) assert.Len(t, intValues, 4) } +func TestSetOperations(t *testing.T) { + a := []string{"a", "b", "b", "c"} + b := []string{"b", "c", "d", "d"} + + assert.ElementsMatch(t, []string{"a", "b", "c", "d"}, Union[string](a, b)) + assert.ElementsMatch(t, UniqueEntries[string](a), Union[string](a, nil)) + assert.ElementsMatch(t, UniqueEntries[string](b), Union[string](nil, b)) + assert.Empty(t, Union[string](nil, nil)) + assert.ElementsMatch(t, []string{"b", "c"}, Intersection[string](a, b)) + assert.Empty(t, Intersection[string](nil, b)) + assert.Empty(t, Intersection[string](nil, nil)) + assert.Empty(t, Intersection[string](a, nil)) + assert.ElementsMatch(t, []string{"a"}, Difference[string](a, b)) + assert.Empty(t, Difference[string](nil, b)) + assert.Empty(t, Difference[string](nil, nil)) + assert.ElementsMatch(t, UniqueEntries[string](a), Difference[string](a, nil)) + assert.ElementsMatch(t, []string{"a", "d"}, SymmetricDifference[string](a, b)) + assert.ElementsMatch(t, UniqueEntries[string](a), SymmetricDifference[string](a, nil)) + assert.Empty(t, SymmetricDifference[string](nil, nil)) + assert.ElementsMatch(t, UniqueEntries[string](b), SymmetricDifference[string](nil, b)) + + entry := []int{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4} + assert.ElementsMatch(t, UniqueEntries[int](entry), Union[int](entry, entry)) + assert.ElementsMatch(t, UniqueEntries[int](entry), Intersection[int](entry, entry)) + assert.Empty(t, Difference[int](entry, entry)) + assert.Empty(t, SymmetricDifference[int](entry, entry)) +} + func TestAnyFunc(t *testing.T) { f := func(v bool) bool { return v @@ -124,6 +186,9 @@ func TestFilterReject(t *testing.T) { assert.ElementsMatch(t, []int{1, 3, 5}, Reject(nums, func(n int) bool { return n%2 == 0 })) + assert.ElementsMatch(t, []int{1, 3, 5}, RejectSequence(slices.Values(nums), func(n int) bool { + return n%2 == 0 + })) assert.ElementsMatch(t, []int{4, 5}, Filter(nums, func(n int) bool { return n > 3 })) @@ -168,6 +233,9 @@ func TestMap(t *testing.T) { assert.ElementsMatch(t, num, m) _, err = MapWithError[string, int](append(numStr, faker.Word(), "5"), strconv.Atoi) require.Error(t, err) + + mappedInt := Map[int, int](num, IdentityMapFunc[int]()) + assert.ElementsMatch(t, num, mappedInt) } func TestReduce(t *testing.T) { @@ -177,3 +245,34 @@ func TestReduce(t *testing.T) { }) assert.Equal(t, sumOfNums, 15) } + +func TestForEach(t *testing.T) { + var visited []int + list := Range(9, 1000, field.ToOptionalInt(13)) + ForEachValues(func(i int) { + visited = append(visited, i) + }, list...) + assert.ElementsMatch(t, visited, list) +} + +func TestForEachSequence(t *testing.T) { + var visited []int + list := Range(9, 1000, field.ToOptionalInt(13)) + errortest.AssertError(t, Each(slices.Values(list), func(i int) error { + if i > 150 { + return commonerrors.ErrUnsupported + } + visited = append(visited, i) + return nil + }), commonerrors.ErrUnsupported) + assert.ElementsMatch(t, visited, []int{9, 22, 35, 48, 61, 74, 87, 100, 113, 126, 139}) + visited = []int{} + assert.NoError(t, Each(slices.Values(list), func(i int) error { + if i > 150 { + return commonerrors.ErrEOF + } + visited = append(visited, i) + return nil + })) + assert.ElementsMatch(t, visited, []int{9, 22, 35, 48, 61, 74, 87, 100, 113, 126, 139}) +} From 70a73b4a4fac31acdeeb82a54ea34737729d7b94 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Mon, 17 Nov 2025 17:32:34 +0000 Subject: [PATCH 2/4] :green_heart: Linting --- utils/collection/search.go | 5 +++-- utils/collection/search_test.go | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/utils/collection/search.go b/utils/collection/search.go index 32c4e4178d..7f429188e3 100644 --- a/utils/collection/search.go +++ b/utils/collection/search.go @@ -9,10 +9,11 @@ import ( "slices" "strings" - "github.com/ARM-software/golang-utils/utils/commonerrors" - "github.com/ARM-software/golang-utils/utils/safecast" mapset "github.com/deckarep/golang-set/v2" "go.uber.org/atomic" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/safecast" ) // Find looks for an element in a slice. If found it will diff --git a/utils/collection/search_test.go b/utils/collection/search_test.go index e3a5bd41a9..e1e56cde55 100644 --- a/utils/collection/search_test.go +++ b/utils/collection/search_test.go @@ -10,13 +10,13 @@ import ( "strconv" "testing" - "github.com/ARM-software/golang-utils/utils/commonerrors" - "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" - "github.com/ARM-software/golang-utils/utils/field" "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" + "github.com/ARM-software/golang-utils/utils/field" "github.com/ARM-software/golang-utils/utils/safecast" ) From 41c9fd0e142eb7397b8352b45708f53aa30e805a Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Mon, 17 Nov 2025 18:02:28 +0000 Subject: [PATCH 3/4] :bug: fix test --- utils/collection/search_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/collection/search_test.go b/utils/collection/search_test.go index e1e56cde55..158bd34649 100644 --- a/utils/collection/search_test.go +++ b/utils/collection/search_test.go @@ -186,9 +186,9 @@ func TestFilterReject(t *testing.T) { assert.ElementsMatch(t, []int{1, 3, 5}, Reject(nums, func(n int) bool { return n%2 == 0 })) - assert.ElementsMatch(t, []int{1, 3, 5}, RejectSequence(slices.Values(nums), func(n int) bool { + assert.ElementsMatch(t, []int{1, 3, 5}, slices.Collect[int](RejectSequence[int](slices.Values(nums), func(n int) bool { return n%2 == 0 - })) + }))) assert.ElementsMatch(t, []int{4, 5}, Filter(nums, func(n int) bool { return n > 3 })) From 2a4460fb3758397bc429d7d36339b4964eb83a46 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Tue, 18 Nov 2025 09:32:09 +0000 Subject: [PATCH 4/4] Apply suggestions from code review --- utils/collection/conditions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/collection/conditions.go b/utils/collection/conditions.go index 3ce9f351e0..b9d01676a1 100644 --- a/utils/collection/conditions.go +++ b/utils/collection/conditions.go @@ -144,7 +144,7 @@ func AnyTrue(values ...bool) bool { return Any(values) } -// AnyFalseSequence returns true if there is at least one element of the sequence which is false. +// AnyFalseSequence returns true if there is at least one element of the sequence which is false. If the sequence is empty, it also returns true. func AnyFalseSequence(eq iter.Seq[bool]) bool { hasElements := false for e := range eq {