From 768480cfd4cbef3957519c6762511e7d99cb35d2 Mon Sep 17 00:00:00 2001 From: Karn Date: Wed, 17 Sep 2025 23:59:01 +0530 Subject: [PATCH 1/4] add PascalCase --- sx.go | 87 ++++++++++++++++++++++++++++++++++++++++++++ sx_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/sx.go b/sx.go index e2dbc69..43db03b 100644 --- a/sx.go +++ b/sx.go @@ -4,6 +4,7 @@ import ( "slices" "strings" "unicode" + "unicode/utf8" ) // Common separators used for splitting strings @@ -144,3 +145,89 @@ func SplitByCase(s string, opts ...SplitOption) []string { return splitByCaseWithCustomSeparators(s, config.Separators) } + +// normalizeWord normalizes a word's case if needed +func normalizeWord(word string, normalize bool) string { + if normalize { + return strings.ToLower(word) + } + return word +} + +// capitalizeWord capitalizes the first letter of a word +func capitalizeWord(word string) string { + if word == "" { + return word + } + + r, size := utf8.DecodeRuneInString(word) + if size == 0 { + return word + } + + return string(unicode.ToUpper(r)) + word[size:] +} + +// joinWords joins words with a separator +func joinWords(words []string, separator string, transform func(string, int) string) string { + if len(words) == 0 { + return "" + } + + var result strings.Builder + for i, word := range words { + if i > 0 && separator != "" { + result.WriteString(separator) + } + result.WriteString(transform(word, i)) + } + + return result.String() +} + +type CaseOption func(*CaseConfig) + +// CaseConfig configures case conversion behavior +type CaseConfig struct { + Normalize bool // If an uppercase letter is followed by other uppercase letters (like FooBAR), they are preserved. You can use { normalize: true } for strictly following pascalCase convention. +} + +// WithNormalize sets the normalize option +func WithNormalize(normalize bool) CaseOption { + return func(c *CaseConfig) { + c.Normalize = normalize + } +} + +// StringOrStringSlice represents input that can be either a string or slice of strings +type StringOrStringSlice interface { + string | []string +} + +// PascalCase converts input to PascalCase +func PascalCase[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, "", 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 { + normalized := normalizeWord(word, options.Normalize) + return capitalizeWord(normalized) + }) + + return result + default: + return "" + } +} diff --git a/sx_test.go b/sx_test.go index 8852088..7d763d3 100644 --- a/sx_test.go +++ b/sx_test.go @@ -169,3 +169,108 @@ func TestSplitByCase_CustomSeparators(t *testing.T) { }) } } + +func TestPascalCase(t *testing.T) { + tests := []struct { + name string + input string + expected string + options []sx.CaseOption + }{ + { + name: "camelCase to PascalCase", + input: "camelCase", + expected: "CamelCase", + }, + { + name: "kebab-case to PascalCase", + input: "kebab-case", + expected: "KebabCase", + }, + { + name: "snake_case to PascalCase", + input: "snake_case", + expected: "SnakeCase", + }, + { + name: "mixed.case_with-separators", + input: "mixed.case_with-separators", + expected: "MixedCaseWithSeparators", + }, + { + 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", + }, + { + name: "hello--world-42", + input: "hello--world-42", + expected: "HelloWorld42", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sx.PascalCase(tt.input, tt.options...) + if result != tt.expected { + t.Errorf("PascalCase(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestPascalCaseWithSlice(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.PascalCase(tt.input, tt.options...) + if result != tt.expected { + t.Errorf("PascalCase(%v) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} From b43d05063fac34643954dd80aba5a57c2b0f5c52 Mon Sep 17 00:00:00 2001 From: Karn Date: Thu, 18 Sep 2025 00:44:05 +0530 Subject: [PATCH 2/4] Update sx.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sx.go b/sx.go index 43db03b..8a129dd 100644 --- a/sx.go +++ b/sx.go @@ -189,7 +189,7 @@ type CaseOption func(*CaseConfig) // CaseConfig configures case conversion behavior type CaseConfig struct { - Normalize bool // If an uppercase letter is followed by other uppercase letters (like FooBAR), they are preserved. You can use { normalize: true } for strictly following pascalCase convention. + Normalize bool // If an uppercase letter is followed by other uppercase letters (like FooBAR), they are preserved. You can use { normalize: true } for strictly following PascalCase convention. } // WithNormalize sets the normalize option From c90bd9fcb75778a32657a6097bb1e1578967d489 Mon Sep 17 00:00:00 2001 From: Karn Date: Thu, 18 Sep 2025 00:49:11 +0530 Subject: [PATCH 3/4] better comment --- sx.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sx.go b/sx.go index 8a129dd..9289f0c 100644 --- a/sx.go +++ b/sx.go @@ -189,7 +189,8 @@ type CaseOption func(*CaseConfig) // CaseConfig configures case conversion behavior type CaseConfig struct { - Normalize bool // If an uppercase letter is followed by other uppercase letters (like FooBAR), they are preserved. You can use { normalize: true } for strictly following PascalCase convention. + // If an uppercase letter is followed by other uppercase letters (like FooBAR), they are preserved. You can use sx.WithNormalize(true) for strictly following PascalCase convention. + Normalize bool } // WithNormalize sets the normalize option From dcbdbfd6e9c9c3518aa07a33f20a8cab12105409 Mon Sep 17 00:00:00 2001 From: Karn Date: Thu, 18 Sep 2025 00:51:38 +0530 Subject: [PATCH 4/4] add header key --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42c7b86..2f9cd41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,7 @@ jobs: uses: marocchino/sticky-pull-request-comment@v2 if: github.event_name == 'pull_request' with: + header: coverage-report recreate: true path: coverage_summary.md