feat: query_parser + stringcase.
This commit is contained in:
65
stringcase/camel_case.go
Normal file
65
stringcase/camel_case.go
Normal 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, "")
|
||||
}
|
||||
111
stringcase/camel_case_test.go
Normal file
111
stringcase/camel_case_test.go
Normal 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
9
stringcase/kebab_case.go
Normal 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)
|
||||
}
|
||||
49
stringcase/kebab_case_test.go
Normal file
49
stringcase/kebab_case_test.go
Normal 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
50
stringcase/snake_case.go
Normal 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))
|
||||
}
|
||||
85
stringcase/snake_case_test.go
Normal file
85
stringcase/snake_case_test.go
Normal 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
144
stringcase/split.go
Normal 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
82
stringcase/split_test.go
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
132
stringcase/utils.go
Normal 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
52
stringcase/utils_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user