diff --git a/sx.go b/sx.go index 9289f0c..ac46d87 100644 --- a/sx.go +++ b/sx.go @@ -232,3 +232,47 @@ func PascalCase[T StringOrStringSlice](input T, opts ...CaseOption) string { return "" } } + +// lowercaseWord converts the first letter to lowercase +func lowercaseWord(word string) string { + if word == "" { + return word + } + + r, size := utf8.DecodeRuneInString(word) + if size == 0 { + return word + } + + return string(unicode.ToLower(r)) + word[size:] +} + +// CamelCase converts input to camelCase +func CamelCase[T StringOrStringSlice](input T, opts ...CaseOption) string { + switch v := any(input).(type) { + case string: + pascalCase := PascalCase(v, opts...) + return lowercaseWord(pascalCase) + case []string: + if len(v) == 0 { + return "" + } + + options := CaseConfig{} + for _, opt := range opts { + opt(&options) + } + + result := joinWords(v, "", func(word string, i int) string { + normalized := normalizeWord(word, options.Normalize) + if i == 0 { + return lowercaseWord(normalized) + } + + return capitalizeWord(normalized) + }) + return result + default: + return "" + } +} diff --git a/sx_test.go b/sx_test.go index 7d763d3..771b927 100644 --- a/sx_test.go +++ b/sx_test.go @@ -274,3 +274,98 @@ func TestPascalCaseWithSlice(t *testing.T) { }) } } + +func TestCamelCase(t *testing.T) { + tests := []struct { + name string + input string + expected string + options []sx.CaseOption + }{ + { + name: "PascalCase to camelCase", + input: "PascalCase", + expected: "pascalCase", + }, + { + name: "kebab-case to camelCase", + input: "kebab-case", + expected: "kebabCase", + }, + { + name: "snake_case to camelCase", + input: "snake_case", + expected: "snakeCase", + }, + { + name: "XMLHttpRequest", + input: "XMLHttpRequest", + expected: "xMLHttpRequest", + }, + { + name: "XMLHttpRequest normalized", + input: "XMLHttpRequest", + expected: "xmlHttpRequest", + 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.CamelCase(tt.input, tt.options...) + if result != tt.expected { + t.Errorf("CamelCase(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestCamelCaseWithSlice(t *testing.T) { + tests := []struct { + name string + input []string + expected string + options []sx.CaseOption + }{ + { + name: "string slice", + input: []string{"hello", "world", "test"}, + expected: "helloWorldTest", + }, + { + name: "string slice normalized", + input: []string{"HELLO", "WORLD", "TEST"}, + expected: "helloWorldTest", + options: []sx.CaseOption{sx.WithNormalize(true)}, + }, + { + name: "empty slice", + input: []string{}, + expected: "", + }, + { + name: "single item slice", + input: []string{"Word"}, + expected: "word", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sx.CamelCase(tt.input, tt.options...) + if result != tt.expected { + t.Errorf("CamelCase(%v) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +}