Skip to content

Commit 4b8dbed

Browse files
committed
text/templates: fix strings functions
1 parent ecf17b5 commit 4b8dbed

File tree

3 files changed

+325
-87
lines changed

3 files changed

+325
-87
lines changed

stringutil/stringutil.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package stringutil
2+
3+
import (
4+
"strings"
5+
"unicode"
6+
)
7+
8+
// Capitalize returns the string with the first letter capitalized.
9+
func Capitalize(s string) string {
10+
if s == "" {
11+
return s
12+
}
13+
r := []rune(s)
14+
return string(unicode.ToUpper(r[0])) + string(r[1:])
15+
}
16+
17+
// Uncapitalize returns the string with the first letter uncapitalized.
18+
func Uncapitalize(s string) string {
19+
if s == "" {
20+
return s
21+
}
22+
r := []rune(s)
23+
return string(unicode.ToLower(r[0])) + string(r[1:])
24+
}
25+
26+
// Rename renames the string with the given convert function and separator.
27+
func Rename(s string, convert func(int, string) string, sep string) string {
28+
if s == "" {
29+
return ""
30+
}
31+
32+
var result strings.Builder
33+
var word strings.Builder
34+
var count int
35+
36+
runes := []rune(s)
37+
n := len(runes)
38+
i := 0
39+
40+
for i < n {
41+
// Skip non-alphanumeric characters, treat them as word boundaries
42+
for i < n && !unicode.IsLetter(runes[i]) && !unicode.IsDigit(runes[i]) {
43+
i++
44+
}
45+
46+
if i >= n {
47+
break
48+
}
49+
50+
word.Reset()
51+
52+
// Collect characters for the current word
53+
for i < n && (unicode.IsLetter(runes[i]) || unicode.IsDigit(runes[i])) {
54+
r := runes[i]
55+
word.WriteRune(r)
56+
i++
57+
58+
if i < n && isWordBoundary(r, runes[i], i, runes) {
59+
break
60+
}
61+
}
62+
63+
if word.Len() > 0 {
64+
if result.Len() > 0 && sep != "" {
65+
result.WriteString(sep)
66+
}
67+
68+
convertedWord := convert(count, word.String())
69+
result.WriteString(convertedWord)
70+
count++
71+
}
72+
}
73+
74+
return result.String()
75+
}
76+
77+
func isWordBoundary(prev rune, curr rune, index int, runes []rune) bool {
78+
// If previous character is lowercase and current is uppercase, it's a boundary
79+
if unicode.IsLower(prev) && unicode.IsUpper(curr) {
80+
return true
81+
}
82+
83+
// Handle acronyms (e.g., "HTTPServer")
84+
if unicode.IsUpper(prev) && unicode.IsUpper(curr) {
85+
// If next character exists and is lowercase, split before current character
86+
if index+1 < len(runes) && unicode.IsLower(runes[index+1]) {
87+
return true
88+
}
89+
return false
90+
}
91+
92+
// If previous is digit and current is letter, it's a boundary
93+
if unicode.IsDigit(prev) && unicode.IsLetter(curr) {
94+
return true
95+
}
96+
97+
// Do not split when transitioning from letter to digit
98+
if unicode.IsLetter(prev) && unicode.IsDigit(curr) {
99+
return false
100+
}
101+
102+
// If both are letters (regardless of case), do not split
103+
if unicode.IsLetter(prev) && unicode.IsLetter(curr) {
104+
return false
105+
}
106+
107+
// If both are digits, do not split
108+
if unicode.IsDigit(prev) && unicode.IsDigit(curr) {
109+
return false
110+
}
111+
112+
// If one is letter/digit and the other is not, it's a boundary
113+
if (unicode.IsLetter(prev) || unicode.IsDigit(prev)) != (unicode.IsLetter(curr) || unicode.IsDigit(curr)) {
114+
return true
115+
}
116+
117+
// Default to no boundary
118+
return false
119+
}
120+
121+
func lowerAll(i int, s string) string {
122+
return strings.ToLower(s)
123+
}
124+
125+
func capitalizeAll(i int, s string) string {
126+
return Capitalize(s)
127+
}
128+
129+
func capitalizeExceptFirst(i int, s string) string {
130+
if i == 0 {
131+
return strings.ToLower(s)
132+
}
133+
return Capitalize(s)
134+
}
135+
136+
// SnakeCase converts the string to snake_case.
137+
func SnakeCase(s string) string {
138+
return Rename(s, lowerAll, "_")
139+
}
140+
141+
// KebabCase converts the string to kebab-case.
142+
func KebabCase(s string) string {
143+
return Rename(s, lowerAll, "-")
144+
}
145+
146+
// CamelCase converts the string to camelCase.
147+
func CamelCase(s string) string {
148+
return Rename(s, capitalizeExceptFirst, "")
149+
}
150+
151+
// PascalCase converts the string to PascalCase.
152+
func PascalCase(s string) string {
153+
return Rename(s, capitalizeAll, "")
154+
}

stringutil/stringutil_test.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package stringutil
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestCapitalize(t *testing.T) {
8+
tests := []struct {
9+
input string
10+
want string
11+
}{
12+
{"", ""},
13+
{"a", "A"},
14+
{"A", "A"},
15+
{"hello", "Hello"},
16+
{"Hello", "Hello"},
17+
{"1hello", "1hello"},
18+
{" hello", " hello"},
19+
{"hELLO", "HELLO"},
20+
{"h", "H"},
21+
}
22+
23+
for _, tt := range tests {
24+
got := Capitalize(tt.input)
25+
if got != tt.want {
26+
t.Errorf("Capitalize(%q) = %q; want %q", tt.input, got, tt.want)
27+
}
28+
}
29+
}
30+
31+
func TestUncapitalize(t *testing.T) {
32+
tests := []struct {
33+
input string
34+
want string
35+
}{
36+
{"", ""},
37+
{"A", "a"},
38+
{"a", "a"},
39+
{"Hello", "hello"},
40+
{"hello", "hello"},
41+
{"1Hello", "1Hello"},
42+
{" Hello", " Hello"},
43+
{"HELLO", "hELLO"},
44+
{"H", "h"},
45+
}
46+
47+
for _, tt := range tests {
48+
got := Uncapitalize(tt.input)
49+
if got != tt.want {
50+
t.Errorf("Uncapitalize(%q) = %q; want %q", tt.input, got, tt.want)
51+
}
52+
}
53+
}
54+
55+
func TestSnakeCase(t *testing.T) {
56+
tests := []struct {
57+
input string
58+
want string
59+
}{
60+
{"", ""},
61+
{"simple", "simple"},
62+
{"SimpleTestCase", "simple_test_case"},
63+
{"HTTPServer", "http_server"},
64+
{"xmlHTTPRequest", "xml_http_request"},
65+
{"MyID", "my_id"},
66+
{"My123ID", "my123_id"},
67+
{"HelloWorld", "hello_world"},
68+
{"helloWorld", "hello_world"},
69+
{"hello_world", "hello_world"},
70+
{"Hello_World", "hello_world"},
71+
{"hello-world", "hello_world"},
72+
}
73+
74+
for _, tt := range tests {
75+
got := SnakeCase(tt.input)
76+
if got != tt.want {
77+
t.Errorf("SnakeCase(%q) = %q; want %q", tt.input, got, tt.want)
78+
}
79+
}
80+
}
81+
82+
func TestKebabCase(t *testing.T) {
83+
tests := []struct {
84+
input string
85+
want string
86+
}{
87+
{"", ""},
88+
{"simple", "simple"},
89+
{"SimpleTestCase", "simple-test-case"},
90+
{"HTTPServer", "http-server"},
91+
{"xmlHTTPRequest", "xml-http-request"},
92+
{"MyID", "my-id"},
93+
{"My123ID", "my123-id"},
94+
{"HelloWorld", "hello-world"},
95+
{"helloWorld", "hello-world"},
96+
{"hello_world", "hello-world"},
97+
{"Hello_World", "hello-world"},
98+
}
99+
100+
for _, tt := range tests {
101+
got := KebabCase(tt.input)
102+
if got != tt.want {
103+
t.Errorf("KebabCase(%q) = %q; want %q", tt.input, got, tt.want)
104+
}
105+
}
106+
}
107+
108+
func TestCamelCase(t *testing.T) {
109+
tests := []struct {
110+
input string
111+
want string
112+
}{
113+
{"", ""},
114+
{"simple", "simple"},
115+
{"SimpleTestCase", "simpleTestCase"},
116+
{"HTTPServer", "httpServer"},
117+
{"xmlHTTPRequest", "xmlHTTPRequest"},
118+
{"MyID", "myID"},
119+
{"My123ID", "my123ID"},
120+
{"HelloWorld", "helloWorld"},
121+
{"helloWorld", "helloWorld"},
122+
{"hello_world", "helloWorld"},
123+
{"Hello_World", "helloWorld"},
124+
{"hello world", "helloWorld"},
125+
{"hello World", "helloWorld"},
126+
{"Hello World", "helloWorld"},
127+
{"hello-world", "helloWorld"},
128+
{"hello-World", "helloWorld"},
129+
{"Hello-World", "helloWorld"},
130+
}
131+
132+
for _, tt := range tests {
133+
got := CamelCase(tt.input)
134+
if got != tt.want {
135+
t.Errorf("CamelCase(%q) = %q; want %q", tt.input, got, tt.want)
136+
}
137+
}
138+
}
139+
140+
func TestPascalCase(t *testing.T) {
141+
tests := []struct {
142+
input string
143+
want string
144+
}{
145+
{"", ""},
146+
{"simple", "Simple"},
147+
{"SimpleTestCase", "SimpleTestCase"},
148+
{"HTTPServer", "HTTPServer"},
149+
{"xmlHTTPRequest", "XmlHTTPRequest"},
150+
{"MyID", "MyID"},
151+
{"My123ID", "My123ID"},
152+
{"HelloWorld", "HelloWorld"},
153+
{"helloWorld", "HelloWorld"},
154+
{"hello_world", "HelloWorld"},
155+
{"Hello_World", "HelloWorld"},
156+
}
157+
158+
for _, tt := range tests {
159+
got := PascalCase(tt.input)
160+
if got != tt.want {
161+
t.Errorf("PascalCase(%q) = %q; want %q", tt.input, got, tt.want)
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)