From 76d84a7f3eb3cb1fc534c6533a89aa17a3b0f39d Mon Sep 17 00:00:00 2001 From: Karn Date: Thu, 18 Sep 2025 02:39:45 +0530 Subject: [PATCH 1/3] add sx.CamelCase --- sx.go | 44 +++++++++++++++++++++++++ sx_test.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) 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) + } + }) + } +} From 29c05f2e699b1c317ab104a64e2e036fc98c73d7 Mon Sep 17 00:00:00 2001 From: Karn Date: Thu, 18 Sep 2025 03:20:41 +0530 Subject: [PATCH 2/3] add more case utils --- sx.go | 92 ++++++++++++++++++-- sx_test.go | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+), 5 deletions(-) diff --git a/sx.go b/sx.go index ac46d87..6ffed62 100644 --- a/sx.go +++ b/sx.go @@ -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) } @@ -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) }) @@ -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) @@ -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) +} diff --git a/sx_test.go b/sx_test.go index 771b927..6f6d1cc 100644 --- a/sx_test.go +++ b/sx_test.go @@ -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) + } + }) + } +} From 490f1060c6d8597f09cd426c14852d653bfaf95c Mon Sep 17 00:00:00 2001 From: Karn Date: Thu, 18 Sep 2025 03:24:02 +0530 Subject: [PATCH 3/3] add pr command --- .cursor/commands/pr.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .cursor/commands/pr.md diff --git a/.cursor/commands/pr.md b/.cursor/commands/pr.md new file mode 100644 index 0000000..45c01c0 --- /dev/null +++ b/.cursor/commands/pr.md @@ -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.