diff --git a/pkg/strutil/strutil.go b/pkg/strutil/strutil.go index 463dccba8..2039722b1 100644 --- a/pkg/strutil/strutil.go +++ b/pkg/strutil/strutil.go @@ -17,7 +17,11 @@ limitations under the License. // Package strutil contains string and byte processing functions. package strutil -import "strings" +import ( + "strings" + "unicode" + "unicode/utf8" +) // Fork of Go's implementation in pkg/strings/strings.go: // Generic split: splits after each instance of sep, @@ -75,8 +79,28 @@ func HasSuffixFold(s, suffix string) bool { return strings.EqualFold(s[len(s)-len(suffix):], suffix) } -// ContainsFold is like strings.Contains but (ought to) use Unicode case-folding. +// ContainsFold is like strings.Contains but uses Unicode case-folding. func ContainsFold(s, substr string) bool { - // TODO: Make this not do allocations. - return strings.Contains(strings.ToLower(s), strings.ToLower(substr)) + if substr == "" { + return true + } + if s == "" { + return false + } + firstRune := rune(substr[0]) + if firstRune >= utf8.RuneSelf { + firstRune, _ = utf8.DecodeRuneInString(substr) + } + firstLowerRune := unicode.SimpleFold(firstRune) + for i, rune := range s { + if len(s)-i < len(substr) { + return false + } + if rune == firstLowerRune || unicode.SimpleFold(rune) == firstLowerRune { + if HasPrefixFold(s[i:], substr) { + return true + } + } + } + return false } diff --git a/pkg/strutil/strutil_test.go b/pkg/strutil/strutil_test.go index 5acc81aa2..b35eebecd 100644 --- a/pkg/strutil/strutil_test.go +++ b/pkg/strutil/strutil_test.go @@ -95,3 +95,44 @@ func TestHasSuffixFold(t *testing.T) { } } } + +func TestContainsFold(t *testing.T) { + // TODO: more tests, more languages. + // The k,K,Kelvin (for now failing) example once TODO in HasPrefixFold is fixed. + tests := []struct { + s, substr string + result bool + }{ + {"camli", "CAML", true}, + {"CAMLI", "caml", true}, + {"cam", "Cam", true}, + {"мир", "ми", true}, + {"МИP", "ми", true}, + {"КАМЛИЙСТОР", "камлийс", true}, + {"КаМлИйСтОр", "КаМлИйС", true}, + {"camli", "car", false}, + {"caml", "camli", false}, + + {"camli", "AMLI", true}, + {"CAMLI", "amli", true}, + {"mli", "MLI", true}, + {"мир", "ир", true}, + {"МИP", "ми", true}, + {"КАМЛИЙСТОР", "лийстор", true}, + {"КаМлИйСтОр", "лИйСтОр", true}, + {"мир", "р", true}, + {"camli", "ali", false}, + {"amli", "camli", false}, + + {"МИP", "и", true}, + {"мир", "и", true}, + {"КАМЛИЙСТОР", "лийс", true}, + {"КаМлИйСтОр", "лИйС", true}, + } + for _, tt := range tests { + r := ContainsFold(tt.s, tt.substr) + if r != tt.result { + t.Errorf("ContainsFold(%q, %q) returned %v", tt.s, tt.substr, r) + } + } +}