mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-08 09:07:44 +00:00
util/stringsx: add package for extra string functions, like CompareFold
Noted as useful during review of #14448. Updates #14457 Change-Id: I0f16f08d5b05a8e9044b19ef6c02d3dab497f131 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
4267d0fc5b
commit
9e2819b5d4
52
util/stringsx/stringsx.go
Normal file
52
util/stringsx/stringsx.go
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package stringsx provides additional string manipulation functions
|
||||
// that aren't in the standard library's strings package or go4.org/mem.
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// CompareFold returns -1, 0, or 1 depending on whether a < b, a == b, or a > b,
|
||||
// like cmp.Compare, but case insensitively.
|
||||
func CompareFold(a, b string) int {
|
||||
// Track our position in both strings
|
||||
ia, ib := 0, 0
|
||||
for ia < len(a) && ib < len(b) {
|
||||
ra, wa := nextRuneLower(a[ia:])
|
||||
rb, wb := nextRuneLower(b[ib:])
|
||||
if ra < rb {
|
||||
return -1
|
||||
}
|
||||
if ra > rb {
|
||||
return 1
|
||||
}
|
||||
ia += wa
|
||||
ib += wb
|
||||
if wa == 0 || wb == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we've reached here, one or both strings are exhausted
|
||||
// The shorter string is "less than" if they match up to this point
|
||||
switch {
|
||||
case ia == len(a) && ib == len(b):
|
||||
return 0
|
||||
case ia == len(a):
|
||||
return -1
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// nextRuneLower returns the next rune in the string, lowercased, along with its
|
||||
// original (consumed) width in bytes. If the string is empty, it returns
|
||||
// (utf8.RuneError, 0)
|
||||
func nextRuneLower(s string) (r rune, width int) {
|
||||
r, width = utf8.DecodeRuneInString(s)
|
||||
return unicode.ToLower(r), width
|
||||
}
|
78
util/stringsx/stringsx_test.go
Normal file
78
util/stringsx/stringsx_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package stringsx
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareFold(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b string
|
||||
}{
|
||||
// Basic ASCII cases
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"a", "A"},
|
||||
{"A", "a"},
|
||||
{"a", "b"},
|
||||
{"b", "a"},
|
||||
{"abc", "ABC"},
|
||||
{"ABC", "abc"},
|
||||
{"abc", "abd"},
|
||||
{"abd", "abc"},
|
||||
|
||||
// Length differences
|
||||
{"abc", "ab"},
|
||||
{"ab", "abc"},
|
||||
|
||||
// Unicode cases
|
||||
{"世界", "世界"},
|
||||
{"Hello世界", "hello世界"},
|
||||
{"世界Hello", "世界hello"},
|
||||
{"世界", "世界x"},
|
||||
{"世界x", "世界"},
|
||||
|
||||
// Special case folding examples
|
||||
{"ß", "ss"}, // German sharp s
|
||||
{"fi", "fi"}, // fi ligature
|
||||
{"Σ", "σ"}, // Greek sigma
|
||||
{"İ", "i\u0307"}, // Turkish dotted I
|
||||
|
||||
// Mixed cases
|
||||
{"HelloWorld", "helloworld"},
|
||||
{"HELLOWORLD", "helloworld"},
|
||||
{"helloworld", "HELLOWORLD"},
|
||||
{"HelloWorld", "helloworld"},
|
||||
{"helloworld", "HelloWorld"},
|
||||
|
||||
// Edge cases
|
||||
{" ", " "},
|
||||
{"1", "1"},
|
||||
{"123", "123"},
|
||||
{"!@#", "!@#"},
|
||||
}
|
||||
|
||||
wants := []int{}
|
||||
for _, tt := range tests {
|
||||
got := CompareFold(tt.a, tt.b)
|
||||
want := cmp.Compare(strings.ToLower(tt.a), strings.ToLower(tt.b))
|
||||
if got != want {
|
||||
t.Errorf("CompareFold(%q, %q) = %v, want %v", tt.a, tt.b, got, want)
|
||||
}
|
||||
wants = append(wants, want)
|
||||
}
|
||||
|
||||
if n := testing.AllocsPerRun(1000, func() {
|
||||
for i, tt := range tests {
|
||||
if CompareFold(tt.a, tt.b) != wants[i] {
|
||||
panic("unexpected")
|
||||
}
|
||||
}
|
||||
}); n > 0 {
|
||||
t.Errorf("allocs = %v; want 0", int(n))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user