Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .cursor/commands/pr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Create a Pull Request

- Make sure local changes are committed and pushed, if not ask user to do it
- Compare the current branch to origin/main
- Read through the changes and commits
- Raise a pull request using the gh cli, you might have to `unset GITHUB_TOKEN` if auth issues occur.
- Don't ever fork, we work directly on the originally repo with branches
- Use a succint title
- Keep the description to the point and do not hallucinate
- Use a temporary file for PR description and remove it later with rm linux command.
92 changes: 87 additions & 5 deletions sx.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,25 @@ func capitalizeWord(word string) string {
}

// joinWords joins words with a separator
func joinWords(words []string, separator string, transform func(string, int) string) string {
func joinWords(words []string, separator string, preserveEmpty bool, transform func(string, int) string) string {
if len(words) == 0 {
return ""
}

// Filter out empty words if not preserving them
wordsToUse := words
if !preserveEmpty {
var filteredWords []string
for _, word := range words {
if word != "" {
filteredWords = append(filteredWords, word)
}
}
wordsToUse = filteredWords
}

var result strings.Builder
for i, word := range words {
for i, word := range wordsToUse {
if i > 0 && separator != "" {
result.WriteString(separator)
}
Expand Down Expand Up @@ -215,14 +227,14 @@ func PascalCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
switch v := any(input).(type) {
case string:
words := splitByCaseWithCustomSeparators(v, nil)
result := joinWords(words, "", func(word string, i int) string {
result := joinWords(words, "", false, func(word string, i int) string {
normalized := normalizeWord(word, options.Normalize)
return capitalizeWord(normalized)
})

return result
case []string:
result := joinWords(v, "", func(word string, i int) string {
result := joinWords(v, "", false, func(word string, i int) string {
normalized := normalizeWord(word, options.Normalize)
return capitalizeWord(normalized)
})
Expand Down Expand Up @@ -263,7 +275,7 @@ func CamelCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
opt(&options)
}

result := joinWords(v, "", func(word string, i int) string {
result := joinWords(v, "", false, func(word string, i int) string {
normalized := normalizeWord(word, options.Normalize)
if i == 0 {
return lowercaseWord(normalized)
Expand All @@ -276,3 +288,73 @@ func CamelCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
return ""
}
}

// KebabCase converts input to kebab-case
func KebabCase[T StringOrStringSlice](input T, separator ...string) string {
sep := "-"
if len(separator) > 0 {
sep = separator[0]
}

switch v := any(input).(type) {
case string:
words := splitByCaseWithCustomSeparators(v, nil)
result := joinWords(words, sep, true, func(word string, i int) string {
return strings.ToLower(word)
})
return result
case []string:
result := joinWords(v, sep, true, func(word string, i int) string {
return strings.ToLower(word)
})
return result
default:
return ""
}
}

// SnakeCase converts input to snake_case
func SnakeCase[T StringOrStringSlice](input T) string {
return KebabCase(input, "_")
}

// TrainCase converts input to Train-Case
func TrainCase[T StringOrStringSlice](input T, opts ...CaseOption) string {
options := CaseConfig{}
for _, opt := range opts {
opt(&options)
}

switch v := any(input).(type) {
case string:
words := splitByCaseWithCustomSeparators(v, nil)
result := joinWords(words, "-", false, func(word string, i int) string {
normalized := normalizeWord(word, options.Normalize)
return capitalizeWord(normalized)
})
return result
case []string:
result := joinWords(v, "-", false, func(word string, i int) string {
normalized := normalizeWord(word, options.Normalize)
return capitalizeWord(normalized)
})
return result
default:
return ""
}
}

// FlatCase converts input to flatcase (no separators)
func FlatCase[T StringOrStringSlice](input T) string {
return KebabCase(input, "")
}

// UpperFirst converts the first character to uppercase
func UpperFirst(s string) string {
return capitalizeWord(s)
}

// LowerFirst converts the first character to lowercase
func LowerFirst(s string) string {
return lowercaseWord(s)
}
251 changes: 251 additions & 0 deletions sx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,254 @@ func TestCamelCaseWithSlice(t *testing.T) {
})
}
}

func TestKebabCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
separator string
}{
{
name: "camelCase to kebab-case",
input: "camelCase",
expected: "camel-case",
},
{
name: "PascalCase to kebab-case",
input: "PascalCase",
expected: "pascal-case",
},
{
name: "snake_case to kebab-case",
input: "snake_case",
expected: "snake-case",
},
{
name: "XMLHttpRequest to kebab-case",
input: "XMLHttpRequest",
expected: "xml-http-request",
},
{
name: "custom separator",
input: "camelCase",
expected: "camel|case",
separator: "|",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single word",
input: "word",
expected: "word",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result string

if tt.separator != "" {
result = sx.KebabCase(tt.input, tt.separator)
} else {
result = sx.KebabCase(tt.input)
}

if result != tt.expected {
t.Errorf("KebabCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

func TestSnakeCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "camelCase to snake_case",
input: "camelCase",
expected: "camel_case",
},
{
name: "PascalCase to snake_case",
input: "PascalCase",
expected: "pascal_case",
},
{
name: "kebab-case to snake_case",
input: "kebab-case",
expected: "kebab_case",
},
{
name: "XMLHttpRequest to snake_case",
input: "XMLHttpRequest",
expected: "xml_http_request",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single word",
input: "word",
expected: "word",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sx.SnakeCase(tt.input)
if result != tt.expected {
t.Errorf("SnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

func TestTrainCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
options []sx.CaseOption
}{
{
name: "camelCase to Train-Case",
input: "camelCase",
expected: "Camel-Case",
},
{
name: "snake_case to Train-Case",
input: "snake_case",
expected: "Snake-Case",
},
{
name: "XMLHttpRequest to Train-Case",
input: "XMLHttpRequest",
expected: "XML-Http-Request",
},
{
name: "XMLHttpRequest normalized",
input: "XMLHttpRequest",
expected: "Xml-Http-Request",
options: []sx.CaseOption{sx.WithNormalize(true)},
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single word",
input: "word",
expected: "Word",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sx.TrainCase(tt.input, tt.options...)
if result != tt.expected {
t.Errorf("TrainCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

func TestFlatCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "camelCase to flatcase",
input: "camelCase",
expected: "camelcase",
},
{
name: "PascalCase to flatcase",
input: "PascalCase",
expected: "pascalcase",
},
{
name: "kebab-case to flatcase",
input: "kebab-case",
expected: "kebabcase",
},
{
name: "XMLHttpRequest to flatcase",
input: "XMLHttpRequest",
expected: "xmlhttprequest",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single word",
input: "Word",
expected: "word",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sx.FlatCase(tt.input)
if result != tt.expected {
t.Errorf("FlatCase(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

func TestEdgeCases(t *testing.T) {
tests := []struct {
name string
input string
function func(string) string
expected string
}{
{
name: "unicode characters",
input: "helloWörld",
function: func(s string) string { return sx.CamelCase(s) },
expected: "helloWörld",
},
{
name: "numbers in string",
input: "html5Parser",
function: func(s string) string { return sx.PascalCase(s) },
expected: "Html5Parser",
},
{
name: "consecutive uppercase",
input: "HTTPSConnection",
function: func(s string) string { return sx.KebabCase(s) },
expected: "https-connection",
},
{
name: "mixed separators",
input: "hello_world-test.case/example",
function: func(s string) string { return sx.CamelCase(s) },
expected: "helloWorldTestCaseExample",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.function(tt.input)
if result != tt.expected {
t.Errorf("Function(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
Loading