feat: query_parser + stringcase.

This commit is contained in:
Bobo
2025-06-23 23:26:59 +08:00
parent de36ab695d
commit 55c80024c2
22 changed files with 1362 additions and 148 deletions

65
stringcase/camel_case.go Normal file
View File

@@ -0,0 +1,65 @@
package stringcase
import (
"strings"
"unicode"
)
func UpperCamelCase(input string) string {
return camelCase(input, true)
}
func LowerCamelCase(input string) string {
return camelCase(input, false)
}
// ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法CamelCase
func ToPascalCase(input string) string {
return camelCase(input, true)
}
func camelCase(input string, upper bool) string {
input = strings.TrimSpace(input)
if input == "" {
return input
}
// 分割字符串
words := Split(input)
if len(words) == 0 {
return ""
}
filteredWords := make([]string, 0, len(words))
for _, word := range words {
if strings.TrimSpace(word) != "" {
filteredWords = append(filteredWords, word)
}
}
words = filteredWords
if len(words) == 0 {
return ""
}
for i, word := range words {
if word == "" {
continue
}
runes := []rune(word)
if len(runes) > 0 {
if i == 0 && !upper {
runes[0] = unicode.ToLower(runes[0]) // LowerCamelCase首单词首字母小写
} else {
runes[0] = unicode.ToUpper(runes[0]) // UpperCamelCase或后续单词首字母大写
}
for j := 1; j < len(runes); j++ {
runes[j] = unicode.ToLower(runes[j]) // 其余字母统一小写
}
words[i] = string(runes)
}
}
// 合并结果
return strings.Join(words, "")
}

View File

@@ -0,0 +1,111 @@
package stringcase
import (
"testing"
)
func TestUpperCamelCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello world", "HelloWorld"},
{"hello_world", "HelloWorld"},
{"hello-world", "HelloWorld"},
{"hello.world", "HelloWorld"},
{"helloWorld", "HelloWorld"},
{"HelloWorld", "HelloWorld"},
{"HTTPStatusCode", "HttpStatusCode"},
{"ParseURL.DoParse", "ParseUrlDoParse"},
{"ParseUrl.DoParse", "ParseUrlDoParse"},
{"parse_url.do_parse", "ParseUrlDoParse"},
{"convert space", "ConvertSpace"},
{"convert-dash", "ConvertDash"},
{"skip___multiple_underscores", "SkipMultipleUnderscores"},
{"skip multiple spaces", "SkipMultipleSpaces"},
{"skip---multiple-dashes", "SkipMultipleDashes"},
{"", ""},
{"a", "A"},
{"Z", "Z"},
{"special-characters_test", "SpecialCharactersTest"},
{"numbers123test", "Numbers123Test"},
{"hello world!", "HelloWorld"},
{"test@with#symbols", "TestWithSymbols"},
{"complexCase123!@#", "ComplexCase123"},
{"snake_case_string", "SnakeCaseString"},
{"kebab-case-string", "KebabCaseString"},
{"PascalCaseString", "PascalCaseString"},
{"camelCaseString", "CamelCaseString"},
{"HTTPRequest", "HttpRequest"},
{"user ID", "UserId"},
{"UserId", "UserId"},
{"userID", "UserId"},
{"UserID", "UserId"},
{"123NumberPrefix", "123NumberPrefix"},
{"__leading_underscores", "LeadingUnderscores"},
{"trailing_underscores__", "TrailingUnderscores"},
{"multiple___underscores", "MultipleUnderscores"},
{" spaces around ", "SpacesAround"},
}
for _, test := range tests {
result := UpperCamelCase(test.input)
if result != test.expected {
t.Errorf("UpperCamelCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func TestLowerCamelCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello world", "helloWorld"},
{"hello_world", "helloWorld"},
{"hello-world", "helloWorld"},
{"hello.world", "helloWorld"},
{"helloWorld", "helloWorld"},
{"HelloWorld", "helloWorld"},
{"HTTPStatusCode", "httpStatusCode"},
{"ParseURL.DoParse", "parseUrlDoParse"},
{"ParseUrl.DoParse", "parseUrlDoParse"},
{"parse_url.do_parse", "parseUrlDoParse"},
{"convert space", "convertSpace"},
{"convert-dash", "convertDash"},
{"skip___multiple_underscores", "skipMultipleUnderscores"},
{"skip multiple spaces", "skipMultipleSpaces"},
{"skip---multiple-dashes", "skipMultipleDashes"},
{"", ""},
{"a", "a"},
{"Z", "z"},
{"special-characters_test", "specialCharactersTest"},
{"numbers123test", "numbers123Test"},
{"hello world!", "helloWorld"},
{"test@with#symbols", "testWithSymbols"},
{"complexCase123!@#", "complexCase123"},
{"snake_case_string", "snakeCaseString"},
{"kebab-case-string", "kebabCaseString"},
{"PascalCaseString", "pascalCaseString"},
{"camelCaseString", "camelCaseString"},
{"HTTPRequest", "httpRequest"},
{"user ID", "userId"},
{"UserId", "userId"},
{"userID", "userId"},
{"UserID", "userId"},
{"123NumberPrefix", "123NumberPrefix"},
{"__leading_underscores", "leadingUnderscores"},
{"trailing_underscores__", "trailingUnderscores"},
{"multiple___underscores", "multipleUnderscores"},
{" spaces around ", "spacesAround"},
}
for _, test := range tests {
result := LowerCamelCase(test.input)
if result != test.expected {
t.Errorf("LowerCamelCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}

9
stringcase/kebab_case.go Normal file
View File

@@ -0,0 +1,9 @@
package stringcase
func KebabCase(s string) string {
return delimiterCase(s, '-', false)
}
func UpperKebabCase(s string) string {
return delimiterCase(s, '-', true)
}

View File

@@ -0,0 +1,49 @@
package stringcase
import "testing"
func TestKebabCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"HelloWorld", "hello-world"},
{"helloWorld", "hello-world"},
{"Hello World", "hello-world"},
{"hello world!", "hello-world"},
{"Numbers123Test", "numbers-123-test"},
{"", ""},
{"_", ""},
{"__Hello__World__", "hello-world"},
}
for _, test := range tests {
result := KebabCase(test.input)
if result != test.expected {
t.Errorf("KebabCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func TestUpperKebabCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"HelloWorld", "HELLO-WORLD"},
{"helloWorld", "HELLO-WORLD"},
{"Hello World", "HELLO-WORLD"},
{"hello world!", "HELLO-WORLD"},
{"Numbers123Test", "NUMBERS-123-TEST"},
{"", ""},
{"_", ""},
{"__Hello__World__", "HELLO-WORLD"},
}
for _, test := range tests {
result := UpperKebabCase(test.input)
if result != test.expected {
t.Errorf("UpperKebabCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}

50
stringcase/snake_case.go Normal file
View File

@@ -0,0 +1,50 @@
package stringcase
import (
"strings"
)
// ToSnakeCase 把字符转换为 蛇形命名法snake_case
func ToSnakeCase(input string) string {
return SnakeCase(input)
}
func SnakeCase(s string) string {
return delimiterCase(s, '_', false)
}
func UpperSnakeCase(s string) string {
return delimiterCase(s, '_', true)
}
func delimiterCase(input string, delimiter rune, upperCase bool) string {
input = strings.TrimSpace(input)
if input == "" {
return input
}
// 使用 Split 分割字符串
words := Split(input)
filteredWords := make([]string, 0, len(words))
for _, word := range words {
if strings.TrimSpace(word) != "" {
filteredWords = append(filteredWords, word)
}
}
adjustCase := toLower
if upperCase {
adjustCase = toUpper
}
for i, word := range filteredWords {
runes := []rune(word)
for j := 0; j < len(runes); j++ {
runes[j] = adjustCase(runes[j])
}
filteredWords[i] = string(runes)
}
// 使用分隔符连接结果
return strings.Join(filteredWords, string(delimiter))
}

View File

@@ -0,0 +1,85 @@
package stringcase
import (
"testing"
)
func TestToSnakeCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"snake_case", "snake_case"},
{"CamelCase", "camel_case"},
{"lowerCamelCase", "lower_camel_case"},
{"F", "f"},
{"Foo", "foo"},
{"FooB", "foo_b"},
{"FooID", "foo_id"},
{" FooBar\t", "foo_bar"},
{"HTTPStatusCode", "http_status_code"},
{"ParseURL.DoParse", "parse_url_do_parse"},
{"Convert Space", "convert_space"},
{"Convert-dash", "convert_dash"},
{"Skip___MultipleUnderscores", "skip_multiple_underscores"},
{"Skip MultipleSpaces", "skip_multiple_spaces"},
{"Skip---MultipleDashes", "skip_multiple_dashes"},
{"Hello World", "hello_world"},
{"Multiple Words Example", "multiple_words_example"},
{"", ""},
{"A", "a"},
{"z", "z"},
{"Special-Characters_Test", "special_characters_test"},
{"Numbers123Test", "numbers_123_test"},
{"Hello World!", "hello_world"},
{"Test@With#Symbols", "test_with_symbols"},
{"ComplexCase123!@#", "complex_case_123"},
}
for _, test := range tests {
result := ToSnakeCase(test.input)
if result != test.expected {
t.Errorf("ToSnakeCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func TestUpperSnakeCase(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"snake_case", "SNAKE_CASE"},
{"CamelCase", "CAMEL_CASE"},
{"lowerCamelCase", "LOWER_CAMEL_CASE"},
{"F", "F"},
{"Foo", "FOO"},
{"FooB", "FOO_B"},
{"FooID", "FOO_ID"},
{" FooBar\t", "FOO_BAR"},
{"HTTPStatusCode", "HTTP_STATUS_CODE"},
{"ParseURL.DoParse", "PARSE_URL_DO_PARSE"},
{"Convert Space", "CONVERT_SPACE"},
{"Convert-dash", "CONVERT_DASH"},
{"Skip___MultipleUnderscores", "SKIP_MULTIPLE_UNDERSCORES"},
{"Skip MultipleSpaces", "SKIP_MULTIPLE_SPACES"},
{"Skip---MultipleDashes", "SKIP_MULTIPLE_DASHES"},
{"Hello World", "HELLO_WORLD"},
{"Multiple Words Example", "MULTIPLE_WORDS_EXAMPLE"},
{"", ""},
{"A", "A"},
{"z", "Z"},
{"Special-Characters_Test", "SPECIAL_CHARACTERS_TEST"},
{"Numbers123Test", "NUMBERS_123_TEST"},
{"Hello World!", "HELLO_WORLD"},
{"Test@With#Symbols", "TEST_WITH_SYMBOLS"},
{"ComplexCase123!@#", "COMPLEX_CASE_123"},
}
for _, test := range tests {
result := UpperSnakeCase(test.input)
if result != test.expected {
t.Errorf("UpperSnakeCase(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}

144
stringcase/split.go Normal file
View File

@@ -0,0 +1,144 @@
package stringcase
import (
"strings"
"unicode"
"unicode/utf8"
)
type runeInfo struct {
r rune
}
// Checks whether or not the rune represented by rInfo is a digit.
func (rInfo *runeInfo) isDigit() bool {
return unicode.IsDigit(rInfo.r)
}
// Checks whether or not the rune represented by rInfo is an uppercase rune.
func (rInfo *runeInfo) isUppercase() bool {
return unicode.IsUpper(rInfo.r)
}
// A reader designed for reading "CamelCase" strings.
type rdr struct {
input string // The data this reader operates on.
pos int // The position of this reader.
hasNextRune bool // A flag indicating if there's a next rune.
rdRune runeInfo // Information about the last rune that was read.
nxtRune runeInfo // Information about the next rune that's about to be read.
}
// Read the next rune from r.
func (r *rdr) readRune() {
r.rdRune = runeInfo{rune(r.input[r.pos])}
r.pos = r.pos + 1
r.hasNextRune = r.pos < len(r.input)
if r.hasNextRune {
r.nxtRune = runeInfo{rune(r.input[r.pos])}
}
}
// Undo the last rune from r.
func (r *rdr) unreadRune() {
r.pos = r.pos - 1
r.nxtRune = r.rdRune
r.rdRune = runeInfo{rune(r.input[r.pos])}
r.hasNextRune = true // NOTE: An undo operation means that there will be always a next rune.
}
// Verify if the word that's currently read by r is a word that should NOT be split.
// If noSplit contains a word that starts with the word that's currently read by r, this function returns true, false
// otherwise.
func (r *rdr) isNoSplitWord(sIdx int, noSplit []string) bool {
return ContainsFn(noSplit, r.input[sIdx:r.pos+1], func(got, want string) bool {
return strings.HasPrefix(got, want)
})
}
// Read the next part from r.
// Each word in noSplit (if provided) is treated as a word that shouldn't be split.
func (r *rdr) readNextPart(noSplit []string) string {
sIdx := r.pos
r.readRune()
if r.rdRune.isDigit() {
return r.readNumber(sIdx, noSplit)
}
return r.readWord(sIdx, noSplit)
}
// Read and return a number from r.
func (r *rdr) readNumber(sIdx int, noSplit []string) string {
if r.hasNextRune && r.nxtRune.isDigit() {
for r.hasNextRune && (r.nxtRune.isDigit() || r.isNoSplitWord(sIdx, noSplit)) {
r.readRune()
}
return r.input[sIdx:r.pos]
}
return r.input[sIdx:r.pos]
}
// Read and return a word from r.
func (r *rdr) readWord(sIdx int, noSplit []string) string {
if r.hasNextRune && r.nxtRune.isUppercase() {
for r.hasNextRune && (r.nxtRune.isUppercase() || r.isNoSplitWord(sIdx, noSplit)) {
r.readRune()
}
if r.hasNextRune && (!r.nxtRune.isUppercase() && !r.nxtRune.isDigit()) {
r.unreadRune()
}
return r.input[sIdx:r.pos]
}
for r.hasNextRune && (r.isNoSplitWord(sIdx, noSplit) || (!r.nxtRune.isUppercase() && !r.nxtRune.isDigit())) {
r.readRune()
}
return r.input[sIdx:r.pos]
}
// Split reads v treating it as a "CamelCase" and returns the different words.
// If v isn't a valid UTF-8 string, or when v is an empty string, a slice with one element (v) is returned.
// Each word in noSplit (if provided) is treated as a word that shouldn't be split.
func Split(input string, noSplit ...string) []string {
if !utf8.ValidString(input) || len(input) == 0 {
return []string{input}
}
output := make([]string, 0)
inputs := SplitByNonAlphanumeric(input)
for _, v := range inputs {
v = strings.TrimSpace(v)
if v == "" {
continue
}
output = append(output, split(v, noSplit...)...)
}
return output
}
func split(input string, noSplit ...string) []string {
if !utf8.ValidString(input) || len(input) == 0 {
return []string{input}
}
vRdr := &rdr{input: input}
output := make([]string, 0)
for vRdr.pos < len(input) {
part := vRdr.readNextPart(noSplit)
output = append(output, part)
}
return output
}

82
stringcase/split_test.go Normal file
View File

@@ -0,0 +1,82 @@
package stringcase
import (
"testing"
)
func TestSplitSingle(t *testing.T) {
input := "URL.DoParse"
result := Split(input)
t.Logf("Split(%q) = %q;", input, result)
t.Log(input[:7])
}
func TestSplit(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"hello world", []string{"hello", "world"}},
{"hello_world", []string{"hello", "world"}},
{"hello-world", []string{"hello", "world"}},
{"hello.world", []string{"hello", "world"}},
{"helloWorld", []string{"hello", "World"}},
{"HelloWorld", []string{"Hello", "World"}},
{"HTTPStatusCode", []string{"HTTP", "Status", "Code"}},
{"ParseURLDoParse", []string{"Parse", "URL", "Do", "Parse"}},
{"ParseUrlDoParse", []string{"Parse", "Url", "Do", "Parse"}},
{"ParseUrl.DoParse", []string{"Parse", "Url", "Do", "Parse"}},
{"ParseURL.DoParse", []string{"Parse", "URL", "Do", "Parse"}},
{"ParseURL", []string{"Parse", "URL"}},
{"ParseURL.", []string{"Parse", "URL"}},
{"parse_url.do_parse", []string{"parse", "url", "do", "parse"}},
{"convert space", []string{"convert", "space"}},
{"convert-dash", []string{"convert", "dash"}},
{"skip___multiple_underscores", []string{"skip", "multiple", "underscores"}},
{"skip multiple spaces", []string{"skip", "multiple", "spaces"}},
{"skip---multiple-dashes", []string{"skip", "multiple", "dashes"}},
{"", []string{""}},
{"a", []string{"a"}},
{"Z", []string{"Z"}},
{"special-characters_test", []string{"special", "characters", "test"}},
{"numbers123test", []string{"numbers", "123", "test"}},
{"hello world!", []string{"hello", "world"}},
{"test@with#symbols", []string{"test", "with", "symbols"}},
{"complexCase123!@#", []string{"complex", "Case", "123"}},
{"snake_case_string", []string{"snake", "case", "string"}},
{"kebab-case-string", []string{"kebab", "case", "string"}},
{"PascalCaseString", []string{"Pascal", "Case", "String"}},
{"camelCaseString", []string{"camel", "Case", "String"}},
{"HTTPRequest", []string{"HTTP", "Request"}},
{"user ID", []string{"user", "ID"}},
{"UserId", []string{"User", "Id"}},
{"userID", []string{"user", "ID"}},
{"UserID", []string{"User", "ID"}},
{"123NumberPrefix", []string{"123", "Number", "Prefix"}},
{"__leading_underscores", []string{"leading", "underscores"}},
{"trailing_underscores__", []string{"trailing", "underscores"}},
{"multiple___underscores", []string{"multiple", "underscores"}},
{" spaces around ", []string{"spaces", "around"}},
}
for _, test := range tests {
result := Split(test.input)
if !compareStringSlices(result, test.expected) {
t.Errorf("Split(%q) = %q; expected %q", test.input, result, test.expected)
}
}
}
func compareStringSlices(slice1, slice2 []string) bool {
if len(slice1) != len(slice2) {
return false
}
for i := range slice1 {
if slice1[i] != slice2[i] {
return false
}
}
return true
}

View File

@@ -1,119 +0,0 @@
package stringcase
import (
"strings"
"unicode"
)
// ToSnakeCase 把字符转换为 蛇形命名法snake_case
func ToSnakeCase(input string) string {
if input == "" {
return input
}
if len(input) == 1 {
return strings.ToLower(input)
}
input = strings.Replace(input, " ", "", -1)
source := []rune(input)
dist := strings.Builder{}
dist.Grow(len(input) + len(input)/3) // avoid reallocation memory, 33% ~ 50% is recommended
skipNext := false
for i := 0; i < len(source); i++ {
cur := source[i]
switch cur {
case '-', '_':
dist.WriteRune('_')
skipNext = true
continue
}
if unicode.IsLower(cur) || unicode.IsDigit(cur) {
dist.WriteRune(cur)
continue
}
if i == 0 {
dist.WriteRune(unicode.ToLower(cur))
continue
}
last := source[i-1]
if (!unicode.IsLetter(last)) || unicode.IsLower(last) {
if skipNext {
skipNext = false
} else {
dist.WriteRune('_')
}
dist.WriteRune(unicode.ToLower(cur))
continue
}
// last is upper case
if i < len(source)-1 {
next := source[i+1]
if unicode.IsLower(next) {
if skipNext {
skipNext = false
} else {
dist.WriteRune('_')
}
dist.WriteRune(unicode.ToLower(cur))
continue
}
}
dist.WriteRune(unicode.ToLower(cur))
}
return dist.String()
}
// ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法CamelCase
func ToPascalCase(input string) string {
return toCamelCase(input, true)
}
// ToLowCamelCase 把字符转换为 小驼峰命名法lowerCamelCase
func ToLowCamelCase(input string) string {
return toCamelCase(input, false)
}
func toCamelCase(s string, initCase bool) string {
s = strings.TrimSpace(s)
if s == "" {
return s
}
var uppercaseAcronym = map[string]string{}
if a, ok := uppercaseAcronym[s]; ok {
s = a
}
n := strings.Builder{}
n.Grow(len(s))
capNext := initCase
for i, v := range []byte(s) {
vIsCap := v >= 'A' && v <= 'Z'
vIsLow := v >= 'a' && v <= 'z'
if capNext {
if vIsLow {
v += 'A'
v -= 'a'
}
} else if i == 0 {
if vIsCap {
v += 'a'
v -= 'A'
}
}
if vIsCap || vIsLow {
n.WriteByte(v)
capNext = false
} else if vIsNum := v >= '0' && v <= '9'; vIsNum {
n.WriteByte(v)
capNext = true
} else {
capNext = v == '_' || v == ' ' || v == '-' || v == '.'
}
}
return n.String()
}

View File

@@ -1,24 +0,0 @@
package stringcase
import (
"fmt"
"testing"
)
func TestToLowCamelCase(t *testing.T) {
fmt.Println(ToLowCamelCase("snake_case"))
fmt.Println(ToLowCamelCase("CamelCase"))
fmt.Println(ToLowCamelCase("lowerCamelCase"))
}
func TestToPascalCase(t *testing.T) {
fmt.Println(ToPascalCase("snake_case"))
fmt.Println(ToPascalCase("CamelCase"))
fmt.Println(ToPascalCase("lowerCamelCase"))
}
func TestToSnakeCase(t *testing.T) {
fmt.Println(ToSnakeCase("snake_case"))
fmt.Println(ToSnakeCase("CamelCase"))
fmt.Println(ToSnakeCase("lowerCamelCase"))
}

132
stringcase/utils.go Normal file
View File

@@ -0,0 +1,132 @@
package stringcase
import (
"regexp"
"strings"
"unicode"
)
func isLower(ch rune) bool {
return ch >= 'a' && ch <= 'z'
}
func toLower(ch rune) rune {
if ch >= 'A' && ch <= 'Z' {
return ch + 32
}
return ch
}
func isUpper(ch rune) bool {
return ch >= 'A' && ch <= 'Z'
}
func toUpper(ch rune) rune {
if ch >= 'a' && ch <= 'z' {
return ch - 32
}
return ch
}
func isSpace(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func isDigit(ch rune) bool {
return ch >= '0' && ch <= '9'
}
func isDelimiter(ch rune) bool {
return ch == '-' || ch == '_' || isSpace(ch)
}
type iterFunc func(prev, curr, next rune)
func stringIter(s string, callback iterFunc) {
var prev rune
var curr rune
for _, next := range s {
if curr == 0 {
prev = curr
curr = next
continue
}
callback(prev, curr, next)
prev = curr
curr = next
}
if len(s) > 0 {
callback(prev, curr, 0)
}
}
func isAlpha(r rune) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}
func ReplaceNonAlphanumeric(s string, replacement string) string {
if replacement == "" {
replacement = "_"
}
// 使用正则表达式匹配非英文字母和数字的字符
re := regexp.MustCompile("[^a-zA-Z0-9]+")
// 替换为指定字符
return re.ReplaceAllString(s, replacement)
}
func SplitByNonAlphanumeric(input string) []string {
var builder strings.Builder
for _, r := range input {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
builder.WriteRune(r)
} else {
builder.WriteRune(' ') // 将非英文字符和数字的字符替换为空格
}
}
processedInput := builder.String()
return strings.Fields(processedInput) // 使用空格分割字符串
}
func SplitAndKeepDelimiters(input string) []string {
var result []string
var builder strings.Builder
for _, r := range input {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
builder.WriteRune(r)
} else {
if builder.Len() > 0 {
result = append(result, builder.String())
builder.Reset()
}
result = append(result, string(r)) // 保留分隔符
}
}
if builder.Len() > 0 {
result = append(result, builder.String())
}
return result
}
func ContainsFn[T any](slice []T, value T, predicate func(got, want T) bool) bool {
for _, item := range slice {
if predicate(item, value) {
return true
}
}
return false
}
func isUpperCaseWord(word string) bool {
for _, r := range word {
if !unicode.IsUpper(r) {
return false
}
}
return true
}

52
stringcase/utils_test.go Normal file
View File

@@ -0,0 +1,52 @@
package stringcase
import (
"reflect"
"testing"
)
func TestSplitByNonAlphanumeric(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"hello-world", []string{"hello", "world"}},
{"hello_world", []string{"hello", "world"}},
{"hello.world", []string{"hello", "world"}},
{"hello world", []string{"hello", "world"}},
{"hello123world", []string{"hello123world"}},
{"hello123 world", []string{"hello123", "world"}},
{"hello-world_123", []string{"hello", "world", "123"}},
{"!hello@world#", []string{"hello", "world"}},
}
for _, test := range tests {
result := SplitByNonAlphanumeric(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("SplitByNonAlphanumeric(%q) = %v; expected %v", test.input, result, test.expected)
}
}
}
func TestSplitAndKeepDelimiters(t *testing.T) {
tests := []struct {
input string
expected []string
}{
{"hello-world", []string{"hello", "-", "world"}},
{"hello_world", []string{"hello", "_", "world"}},
{"hello.world", []string{"hello", ".", "world"}},
{"hello world", []string{"hello", " ", "world"}},
{"hello123world", []string{"hello123world"}},
{"hello123 world", []string{"hello123", " ", "world"}},
{"hello-world_123", []string{"hello", "-", "world", "_", "123"}},
{"!hello@world#", []string{"!", "hello", "@", "world", "#"}},
}
for _, test := range tests {
result := SplitAndKeepDelimiters(test.input)
if !reflect.DeepEqual(result, test.expected) {
t.Errorf("SplitAndKeepDelimiters(%q) = %v; expected %v", test.input, result, test.expected)
}
}
}