diff --git a/util/slicesx/slicesx.go b/util/slicesx/slicesx.go index 5f6eb8d91..8abf2bd64 100644 --- a/util/slicesx/slicesx.go +++ b/util/slicesx/slicesx.go @@ -4,7 +4,10 @@ // Package slicesx contains some helpful generic slice functions. package slicesx -import "math/rand/v2" +import ( + "math/rand/v2" + "slices" +) // Interleave combines two slices of the form [a, b, c] and [x, y, z] into a // slice with elements interleaved; i.e. [a, x, b, y, c, z]. @@ -101,3 +104,35 @@ func AppendMatching[T any](dst, ps []T, f func(T) bool) []T { } return dst } + +// HasPrefix reports whether the byte slice s begins with prefix. +func HasPrefix[E comparable](s, prefix []E) bool { + return len(s) >= len(prefix) && slices.Equal(s[0:len(prefix)], prefix) +} + +// HasSuffix reports whether the slice s ends with suffix. +func HasSuffix[E comparable](s, suffix []E) bool { + return len(s) >= len(suffix) && slices.Equal(s[len(s)-len(suffix):], suffix) +} + +// CutPrefix returns s without the provided leading prefix slice and reports +// whether it found the prefix. If s doesn't start with prefix, CutPrefix +// returns s, false. If prefix is the empty slice, CutPrefix returns s, true. +// CutPrefix returns slices of the original slice s, not copies. +func CutPrefix[E comparable](s, prefix []E) (after []E, found bool) { + if !HasPrefix(s, prefix) { + return s, false + } + return s[len(prefix):], true +} + +// CutSuffix returns s without the provided ending suffix slice and reports +// whether it found the suffix. If s doesn't end with suffix, CutSuffix returns +// s, false. If suffix is the empty slice, CutSuffix returns s, true. +// CutSuffix returns slices of the original slice s, not copies. +func CutSuffix[E comparable](s, suffix []E) (after []E, found bool) { + if !HasSuffix(s, suffix) { + return s, false + } + return s[:len(s)-len(suffix)], true +} diff --git a/util/slicesx/slicesx_test.go b/util/slicesx/slicesx_test.go index 854fe824d..be136d288 100644 --- a/util/slicesx/slicesx_test.go +++ b/util/slicesx/slicesx_test.go @@ -151,3 +151,49 @@ func TestAppendMatching(t *testing.T) { t.Errorf("got %v; want %v", v, wantOrigMem) } } + +func TestCutPrefix(t *testing.T) { + tests := []struct { + name string + s, prefix []int + after []int + found bool + }{ + {"has-prefix", []int{1, 2, 3}, []int{1}, []int{2, 3}, true}, + {"exact-prefix", []int{1, 2, 3}, []int{1, 2, 3}, []int{}, true}, + {"blank-prefix", []int{1, 2, 3}, []int{}, []int{1, 2, 3}, true}, + {"no-prefix", []int{1, 2, 3}, []int{42}, []int{1, 2, 3}, false}, + {"blank-slice", []int{}, []int{42}, []int{}, false}, + {"blank-all", []int{}, []int{}, []int{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if after, found := CutPrefix(tt.s, tt.prefix); !slices.Equal(after, tt.after) || found != tt.found { + t.Errorf("CutPrefix(%v, %v) = %v, %v; want %v, %v", tt.s, tt.prefix, after, found, tt.after, tt.found) + } + }) + } +} + +func TestCutSuffix(t *testing.T) { + tests := []struct { + name string + s, suffix []int + before []int + found bool + }{ + {"has-suffix", []int{1, 2, 3}, []int{3}, []int{1, 2}, true}, + {"exact-suffix", []int{1, 2, 3}, []int{1, 2, 3}, []int{}, true}, + {"blank-suffix", []int{1, 2, 3}, []int{}, []int{1, 2, 3}, true}, + {"no-suffix", []int{1, 2, 3}, []int{42}, []int{1, 2, 3}, false}, + {"blank-slice", []int{}, []int{42}, []int{}, false}, + {"blank-all", []int{}, []int{}, []int{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if before, found := CutSuffix(tt.s, tt.suffix); !slices.Equal(before, tt.before) || found != tt.found { + t.Errorf("CutSuffix(%v, %v) = %v, %v; want %v, %v", tt.s, tt.suffix, before, found, tt.before, tt.found) + } + }) + } +}