Skip to content

Commit e29f2a9

Browse files
authored
✨ [collection] Add matching helpers for collection lookups that can be used in switch statements. (#840)
Adding some matching helpers over collection so they can be used in `switch` statements and therefore, avoid a lot of `||` statements
1 parent 9b1a641 commit e29f2a9

5 files changed

Lines changed: 161 additions & 1 deletion

File tree

changes/20260401153128.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: [`collection`] Add matching helpers for collection lookups that can be used in `switch` statements.

utils/collection/filter.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package collection
22

33
import (
44
"iter"
5+
"regexp"
56
"slices"
7+
"strings"
68

79
"github.com/ARM-software/golang-utils/utils/field"
810
)
@@ -34,6 +36,53 @@ func toPredicateFunc[E any](f PredicateRef[E]) Predicate[E] {
3436
}
3537
}
3638

39+
// MatchFunc compares two values of type E and reports whether they match.
40+
// It may return an error if the comparison requires additional processing,
41+
// such as compiling or evaluating a regular expression.
42+
type MatchFunc[E any] func(E, E) (bool, error)
43+
44+
// MatchRefFunc compares two references to values of type E and reports whether
45+
// they match. It may return an error if the comparison requires additional
46+
// processing before the match can be determined.
47+
type MatchRefFunc[E any] func(*E, *E) (bool, error)
48+
49+
func matchToPredicateFunc[E any](v E, matchFunc MatchFunc[E]) Predicate[E] {
50+
return func(e E) bool {
51+
matched, err := matchFunc(v, e)
52+
return matched && err == nil
53+
}
54+
}
55+
56+
func matchToPredicateRefFunc[E any](v *E, matchFunc MatchRefFunc[E]) PredicateRef[E] {
57+
return func(e *E) bool {
58+
matched, err := matchFunc(v, e)
59+
return matched && err == nil
60+
}
61+
}
62+
63+
// StringMatch reports whether two strings are exactly equal.
64+
var StringMatch MatchFunc[string] = func(a, b string) (bool, error) { return a == b, nil }
65+
66+
// StringCaseInsensitiveMatch reports whether two strings are equal but ignoring their case.
67+
var StringCaseInsensitiveMatch MatchFunc[string] = func(a, b string) (bool, error) { return strings.EqualFold(a, b), nil }
68+
69+
// StringCleanCaseInsensitiveMatch reports whether two strings are equal after
70+
// trimming surrounding whitespace and ignoring their case.
71+
var StringCleanCaseInsensitiveMatch MatchFunc[string] = func(a, b string) (bool, error) {
72+
return StringCaseInsensitiveMatch(strings.TrimSpace(a), strings.TrimSpace(b))
73+
}
74+
75+
// StringCleanMatch reports whether two strings are exactly equal after trimming
76+
// surrounding whitespace.
77+
var StringCleanMatch MatchFunc[string] = func(a, b string) (bool, error) { return StringMatch(strings.TrimSpace(a), strings.TrimSpace(b)) }
78+
79+
// StringRegexMatch reports whether a string matches the provided regular expression pattern.
80+
// the pattern being the first argument.
81+
var StringRegexMatch MatchFunc[string] = regexp.MatchString
82+
83+
// StrictRefMatch reports whether two references are equal using field.Equal.
84+
func StrictRefMatch[E comparable](a, b *E) (bool, error) { return field.Equal(a, b), nil }
85+
3786
//
3887
// Rejection / Filtering
3988
//

utils/collection/find.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func Find(slice *[]string, val string) (int, bool) {
2525
}
2626

2727
// FindInSequence searches elements (a sequence) for the first item that
28-
// satisfies predicate. It returns the zero-based index of the matching
28+
// satisfies the predicate. It returns the zero-based index of the matching
2929
// element and true when a match is found. If elements is nil or no
3030
// match exists, it returns -1 and false.
3131
func FindInSequence[E any](elements iter.Seq[E], predicate Predicate[E]) (int, bool) {

utils/collection/match.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package collection
22

3+
import (
4+
"iter"
5+
"slices"
6+
)
7+
38
//
49
// Match utilities
510
//
@@ -22,3 +27,43 @@ func Match[E any](e E, matches ...FilterFunc[E]) bool {
2227
func MatchAll[E any](e E, matches ...FilterFunc[E]) bool {
2328
return match[E](e, matches).All()
2429
}
30+
31+
// InSequence reports whether any element in s matches v using any of the
32+
// provided match functions. Match functions that return an error are treated
33+
// as non-matches.
34+
func InSequence[E any](s iter.Seq[E], v E, m ...MatchFunc[E]) bool {
35+
matching := Map(m, func(mf MatchFunc[E]) Predicate[E] {
36+
return matchToPredicateFunc(v, mf)
37+
})
38+
return matchInSeq(s, matching)
39+
}
40+
41+
// InSequenceRef behaves like InSequence but uses reference-based match
42+
// functions and a reference value.
43+
func InSequenceRef[E any](s iter.Seq[E], v *E, m ...MatchRefFunc[E]) bool {
44+
matching := Map(m, func(mf MatchRefFunc[E]) Predicate[E] {
45+
return toPredicateFunc(matchToPredicateRefFunc(v, mf))
46+
})
47+
return matchInSeq(s, matching)
48+
}
49+
50+
// In reports whether any element in s matches v using any of the provided
51+
// match functions. Match functions that return an error are treated as
52+
// non-matches.
53+
func In[Slice ~[]E, E any](s Slice, v E, m ...MatchFunc[E]) bool {
54+
return InSequence(slices.Values(s), v, m...)
55+
}
56+
57+
// InRef behaves like In but uses reference-based match functions and a
58+
// reference value.
59+
func InRef[Slice ~[]E, E any](s Slice, v *E, m ...MatchRefFunc[E]) bool {
60+
return InSequenceRef(slices.Values(s), v, m...)
61+
}
62+
63+
func matchInSeq[E any](s iter.Seq[E], matching []Predicate[E]) bool {
64+
matchingF := func(e E) bool {
65+
return Match(e, matching...)
66+
}
67+
_, found := FindInSequence(s, matchingF)
68+
return found
69+
}

utils/collection/match_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package collection
2+
3+
import (
4+
"iter"
5+
"slices"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
10+
"github.com/ARM-software/golang-utils/utils/commonerrors"
11+
)
12+
13+
func TestInSequence(t *testing.T) {
14+
values := []string{"alpha", "Beta", "gamma"}
15+
16+
assert.True(t, InSequence(slices.Values(values), " beta ", StringCleanCaseInsensitiveMatch))
17+
assert.False(t, InSequence(slices.Values(values), "beta", StringCleanMatch))
18+
assert.False(t, InSequence(slices.Values(values), "delta", StringCleanCaseInsensitiveMatch))
19+
20+
var nilSeq iter.Seq[string]
21+
assert.False(t, InSequence(nilSeq, "alpha", StringMatch))
22+
23+
errMatch := func(pattern, value string) (bool, error) {
24+
return true, commonerrors.ErrUnexpected
25+
}
26+
assert.False(t, InSequence(slices.Values(values), "alpha", errMatch))
27+
}
28+
29+
func TestInSequenceRef(t *testing.T) {
30+
values := []int{1, 2, 3}
31+
target := 2
32+
33+
assert.True(t, InSequenceRef(slices.Values(values), &target, StrictRefMatch))
34+
35+
missing := 4
36+
assert.False(t, InSequenceRef(slices.Values(values), &missing, StrictRefMatch))
37+
assert.False(t, InSequenceRef(slices.Values(values), &target, func(a, b *int) (bool, error) {
38+
return true, commonerrors.ErrUnexpected
39+
}))
40+
41+
var nilSeq iter.Seq[int]
42+
assert.False(t, InSequenceRef(nilSeq, &target, StrictRefMatch))
43+
}
44+
45+
func TestIn(t *testing.T) {
46+
values := []string{"alpha", "beta", "gamma"}
47+
48+
assert.True(t, In(values, "^be", StringRegexMatch))
49+
assert.False(t, In(values, "^de", StringRegexMatch))
50+
assert.True(t, In(values, "BETA", StringCaseInsensitiveMatch))
51+
assert.True(t, In(values, ".+ta.*", StringRegexMatch))
52+
}
53+
54+
func TestInRef(t *testing.T) {
55+
values := []string{"alpha", "beta", "gamma"}
56+
target := "beta"
57+
58+
assert.True(t, InRef(values, &target, StrictRefMatch))
59+
60+
missing := "delta"
61+
assert.False(t, InRef(values, &missing, StrictRefMatch))
62+
assert.False(t, InRef(values, &target, func(a, b *string) (bool, error) {
63+
return true, commonerrors.ErrUnexpected
64+
}))
65+
}

0 commit comments

Comments
 (0)