Skip to content

Commit 6a4990c

Browse files
authored
Add comprehensive case conversion utilities (#3)
1 parent fee2b75 commit 6a4990c

File tree

3 files changed

+348
-5
lines changed

3 files changed

+348
-5
lines changed

.cursor/commands/pr.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Create a Pull Request
2+
3+
- Make sure local changes are committed and pushed, if not ask user to do it
4+
- Compare the current branch to origin/main
5+
- Read through the changes and commits
6+
- Raise a pull request using the gh cli, you might have to `unset GITHUB_TOKEN` if auth issues occur.
7+
- Don't ever fork, we work directly on the originally repo with branches
8+
- Use a succint title
9+
- Keep the description to the point and do not hallucinate
10+
- Use a temporary file for PR description and remove it later with rm linux command.

sx.go

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,25 @@ func capitalizeWord(word string) string {
169169
}
170170

171171
// joinWords joins words with a separator
172-
func joinWords(words []string, separator string, transform func(string, int) string) string {
172+
func joinWords(words []string, separator string, preserveEmpty bool, transform func(string, int) string) string {
173173
if len(words) == 0 {
174174
return ""
175175
}
176176

177+
// Filter out empty words if not preserving them
178+
wordsToUse := words
179+
if !preserveEmpty {
180+
var filteredWords []string
181+
for _, word := range words {
182+
if word != "" {
183+
filteredWords = append(filteredWords, word)
184+
}
185+
}
186+
wordsToUse = filteredWords
187+
}
188+
177189
var result strings.Builder
178-
for i, word := range words {
190+
for i, word := range wordsToUse {
179191
if i > 0 && separator != "" {
180192
result.WriteString(separator)
181193
}
@@ -215,14 +227,14 @@ func PascalCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
215227
switch v := any(input).(type) {
216228
case string:
217229
words := splitByCaseWithCustomSeparators(v, nil)
218-
result := joinWords(words, "", func(word string, i int) string {
230+
result := joinWords(words, "", false, func(word string, i int) string {
219231
normalized := normalizeWord(word, options.Normalize)
220232
return capitalizeWord(normalized)
221233
})
222234

223235
return result
224236
case []string:
225-
result := joinWords(v, "", func(word string, i int) string {
237+
result := joinWords(v, "", false, func(word string, i int) string {
226238
normalized := normalizeWord(word, options.Normalize)
227239
return capitalizeWord(normalized)
228240
})
@@ -263,7 +275,7 @@ func CamelCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
263275
opt(&options)
264276
}
265277

266-
result := joinWords(v, "", func(word string, i int) string {
278+
result := joinWords(v, "", false, func(word string, i int) string {
267279
normalized := normalizeWord(word, options.Normalize)
268280
if i == 0 {
269281
return lowercaseWord(normalized)
@@ -276,3 +288,73 @@ func CamelCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
276288
return ""
277289
}
278290
}
291+
292+
// KebabCase converts input to kebab-case
293+
func KebabCase[T StringOrStringSlice](input T, separator ...string) string {
294+
sep := "-"
295+
if len(separator) > 0 {
296+
sep = separator[0]
297+
}
298+
299+
switch v := any(input).(type) {
300+
case string:
301+
words := splitByCaseWithCustomSeparators(v, nil)
302+
result := joinWords(words, sep, true, func(word string, i int) string {
303+
return strings.ToLower(word)
304+
})
305+
return result
306+
case []string:
307+
result := joinWords(v, sep, true, func(word string, i int) string {
308+
return strings.ToLower(word)
309+
})
310+
return result
311+
default:
312+
return ""
313+
}
314+
}
315+
316+
// SnakeCase converts input to snake_case
317+
func SnakeCase[T StringOrStringSlice](input T) string {
318+
return KebabCase(input, "_")
319+
}
320+
321+
// TrainCase converts input to Train-Case
322+
func TrainCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
323+
options := CaseConfig{}
324+
for _, opt := range opts {
325+
opt(&options)
326+
}
327+
328+
switch v := any(input).(type) {
329+
case string:
330+
words := splitByCaseWithCustomSeparators(v, nil)
331+
result := joinWords(words, "-", false, func(word string, i int) string {
332+
normalized := normalizeWord(word, options.Normalize)
333+
return capitalizeWord(normalized)
334+
})
335+
return result
336+
case []string:
337+
result := joinWords(v, "-", false, func(word string, i int) string {
338+
normalized := normalizeWord(word, options.Normalize)
339+
return capitalizeWord(normalized)
340+
})
341+
return result
342+
default:
343+
return ""
344+
}
345+
}
346+
347+
// FlatCase converts input to flatcase (no separators)
348+
func FlatCase[T StringOrStringSlice](input T) string {
349+
return KebabCase(input, "")
350+
}
351+
352+
// UpperFirst converts the first character to uppercase
353+
func UpperFirst(s string) string {
354+
return capitalizeWord(s)
355+
}
356+
357+
// LowerFirst converts the first character to lowercase
358+
func LowerFirst(s string) string {
359+
return lowercaseWord(s)
360+
}

sx_test.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,254 @@ func TestCamelCaseWithSlice(t *testing.T) {
369369
})
370370
}
371371
}
372+
373+
func TestKebabCase(t *testing.T) {
374+
tests := []struct {
375+
name string
376+
input string
377+
expected string
378+
separator string
379+
}{
380+
{
381+
name: "camelCase to kebab-case",
382+
input: "camelCase",
383+
expected: "camel-case",
384+
},
385+
{
386+
name: "PascalCase to kebab-case",
387+
input: "PascalCase",
388+
expected: "pascal-case",
389+
},
390+
{
391+
name: "snake_case to kebab-case",
392+
input: "snake_case",
393+
expected: "snake-case",
394+
},
395+
{
396+
name: "XMLHttpRequest to kebab-case",
397+
input: "XMLHttpRequest",
398+
expected: "xml-http-request",
399+
},
400+
{
401+
name: "custom separator",
402+
input: "camelCase",
403+
expected: "camel|case",
404+
separator: "|",
405+
},
406+
{
407+
name: "empty string",
408+
input: "",
409+
expected: "",
410+
},
411+
{
412+
name: "single word",
413+
input: "word",
414+
expected: "word",
415+
},
416+
}
417+
418+
for _, tt := range tests {
419+
t.Run(tt.name, func(t *testing.T) {
420+
var result string
421+
422+
if tt.separator != "" {
423+
result = sx.KebabCase(tt.input, tt.separator)
424+
} else {
425+
result = sx.KebabCase(tt.input)
426+
}
427+
428+
if result != tt.expected {
429+
t.Errorf("KebabCase(%q) = %q, want %q", tt.input, result, tt.expected)
430+
}
431+
})
432+
}
433+
}
434+
435+
func TestSnakeCase(t *testing.T) {
436+
tests := []struct {
437+
name string
438+
input string
439+
expected string
440+
}{
441+
{
442+
name: "camelCase to snake_case",
443+
input: "camelCase",
444+
expected: "camel_case",
445+
},
446+
{
447+
name: "PascalCase to snake_case",
448+
input: "PascalCase",
449+
expected: "pascal_case",
450+
},
451+
{
452+
name: "kebab-case to snake_case",
453+
input: "kebab-case",
454+
expected: "kebab_case",
455+
},
456+
{
457+
name: "XMLHttpRequest to snake_case",
458+
input: "XMLHttpRequest",
459+
expected: "xml_http_request",
460+
},
461+
{
462+
name: "empty string",
463+
input: "",
464+
expected: "",
465+
},
466+
{
467+
name: "single word",
468+
input: "word",
469+
expected: "word",
470+
},
471+
}
472+
473+
for _, tt := range tests {
474+
t.Run(tt.name, func(t *testing.T) {
475+
result := sx.SnakeCase(tt.input)
476+
if result != tt.expected {
477+
t.Errorf("SnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
478+
}
479+
})
480+
}
481+
}
482+
483+
func TestTrainCase(t *testing.T) {
484+
tests := []struct {
485+
name string
486+
input string
487+
expected string
488+
options []sx.CaseOption
489+
}{
490+
{
491+
name: "camelCase to Train-Case",
492+
input: "camelCase",
493+
expected: "Camel-Case",
494+
},
495+
{
496+
name: "snake_case to Train-Case",
497+
input: "snake_case",
498+
expected: "Snake-Case",
499+
},
500+
{
501+
name: "XMLHttpRequest to Train-Case",
502+
input: "XMLHttpRequest",
503+
expected: "XML-Http-Request",
504+
},
505+
{
506+
name: "XMLHttpRequest normalized",
507+
input: "XMLHttpRequest",
508+
expected: "Xml-Http-Request",
509+
options: []sx.CaseOption{sx.WithNormalize(true)},
510+
},
511+
{
512+
name: "empty string",
513+
input: "",
514+
expected: "",
515+
},
516+
{
517+
name: "single word",
518+
input: "word",
519+
expected: "Word",
520+
},
521+
}
522+
523+
for _, tt := range tests {
524+
t.Run(tt.name, func(t *testing.T) {
525+
result := sx.TrainCase(tt.input, tt.options...)
526+
if result != tt.expected {
527+
t.Errorf("TrainCase(%q) = %q, want %q", tt.input, result, tt.expected)
528+
}
529+
})
530+
}
531+
}
532+
533+
func TestFlatCase(t *testing.T) {
534+
tests := []struct {
535+
name string
536+
input string
537+
expected string
538+
}{
539+
{
540+
name: "camelCase to flatcase",
541+
input: "camelCase",
542+
expected: "camelcase",
543+
},
544+
{
545+
name: "PascalCase to flatcase",
546+
input: "PascalCase",
547+
expected: "pascalcase",
548+
},
549+
{
550+
name: "kebab-case to flatcase",
551+
input: "kebab-case",
552+
expected: "kebabcase",
553+
},
554+
{
555+
name: "XMLHttpRequest to flatcase",
556+
input: "XMLHttpRequest",
557+
expected: "xmlhttprequest",
558+
},
559+
{
560+
name: "empty string",
561+
input: "",
562+
expected: "",
563+
},
564+
{
565+
name: "single word",
566+
input: "Word",
567+
expected: "word",
568+
},
569+
}
570+
571+
for _, tt := range tests {
572+
t.Run(tt.name, func(t *testing.T) {
573+
result := sx.FlatCase(tt.input)
574+
if result != tt.expected {
575+
t.Errorf("FlatCase(%q) = %q, want %q", tt.input, result, tt.expected)
576+
}
577+
})
578+
}
579+
}
580+
581+
func TestEdgeCases(t *testing.T) {
582+
tests := []struct {
583+
name string
584+
input string
585+
function func(string) string
586+
expected string
587+
}{
588+
{
589+
name: "unicode characters",
590+
input: "helloWörld",
591+
function: func(s string) string { return sx.CamelCase(s) },
592+
expected: "helloWörld",
593+
},
594+
{
595+
name: "numbers in string",
596+
input: "html5Parser",
597+
function: func(s string) string { return sx.PascalCase(s) },
598+
expected: "Html5Parser",
599+
},
600+
{
601+
name: "consecutive uppercase",
602+
input: "HTTPSConnection",
603+
function: func(s string) string { return sx.KebabCase(s) },
604+
expected: "https-connection",
605+
},
606+
{
607+
name: "mixed separators",
608+
input: "hello_world-test.case/example",
609+
function: func(s string) string { return sx.CamelCase(s) },
610+
expected: "helloWorldTestCaseExample",
611+
},
612+
}
613+
614+
for _, tt := range tests {
615+
t.Run(tt.name, func(t *testing.T) {
616+
result := tt.function(tt.input)
617+
if result != tt.expected {
618+
t.Errorf("Function(%q) = %q, want %q", tt.input, result, tt.expected)
619+
}
620+
})
621+
}
622+
}

0 commit comments

Comments
 (0)