diff --git a/edge_cases_test.go b/edge_cases_test.go index a5db24a..72aac1f 100644 --- a/edge_cases_test.go +++ b/edge_cases_test.go @@ -5,7 +5,7 @@ import ( ) func TestEdgeCases(t *testing.T) { - // 1. Unicode in splitMixCase + // 1. Unicode in Mixed Case Splitting // Even though ExactCaseWord is a single word in the IL, OptionMixCaseSupport // instructs the formatter to split it based on casing. // This test verifies that this splitting works for both ASCII and Unicode. @@ -64,7 +64,7 @@ func TestEdgeCases(t *testing.T) { } }) - // 4. Consecutive Uppercase in splitMixCase + // 4. Consecutive Uppercase in Mixed Case Splitting t.Run("Consecutive Uppercase", func(t *testing.T) { input := []Word{ExactCaseWord("JSONParser")} res := ToFormattedCase(input, OptionMixCaseSupport(), OptionDelimiter("-")) diff --git a/types.go b/types.go index 832cb05..e8ec85a 100644 --- a/types.go +++ b/types.go @@ -41,6 +41,14 @@ func (w AcronymWord) String() string { return string(w) } func (w UpperCaseWord) String() string { return strings.ToUpper(string(w)) } func (w SeparatorWord) String() string { return string(w) } +// Len implementations +func (w SingleCaseWord) Len() int { return len(w) } +func (w FirstUpperCaseWord) Len() int { return len(w) } +func (w ExactCaseWord) Len() int { return len(w) } +func (w AcronymWord) Len() int { return len(w) } +func (w UpperCaseWord) Len() int { return len(w) } +func (w SeparatorWord) Len() int { return len(w) } + func performCaseFirst(s string, fn func(rune) rune) (string, rune, bool) { if s == "" { return s, 0, true @@ -216,79 +224,174 @@ func WordsToFormattedCase(words []Word, opts ...any) (string, error) { cfg.firstUpper = true } - result := make([]string, 0, len(words)) + delimiter := cfg.delimiter + if cfg.upperIndicator != "" { + if cfg.upperIndicator == cfg.delimiter { + delimiter = cfg.delimiter + cfg.delimiter + } else { + delimiter = cfg.upperIndicator + } + } + + size := 0 for _, word := range words { - var w string + if l, ok := word.(interface{ Len() int }); ok { + size += l.Len() + } else { + size += 5 // fallback + } + } + size += len(delimiter) * max(0, len(words)-1) + + var b strings.Builder + b.Grow(size) + + for i, word := range words { + if i > 0 { + b.WriteString(delimiter) + } + switch word := word.(type) { case SingleCaseWord: - w = string(word) + s := string(word) if cfg.allUpper || cfg.screaming { - w = strings.ToUpper(w) + for _, r := range s { + b.WriteRune(unicode.ToUpper(r)) + } } else if cfg.allLower || cfg.whispering { - w = strings.ToLower(w) + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } } else if cfg.caseMode == CMAllTitle { - w = UpperCaseFirst(strings.ToLower(w)) + first := true + for _, r := range s { + if first { + b.WriteRune(unicode.ToUpper(r)) + first = false + } else { + b.WriteRune(unicode.ToLower(r)) + } + } } else { - w = strings.ToLower(w) + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } } case ExactCaseWord: - w = word.String() + s := string(word) if cfg.mixCaseSupport { - w = splitMixCase(w, cfg.delimiter) - } - if cfg.allUpper || cfg.screaming { - w = strings.ToUpper(w) - } else if cfg.allLower || cfg.whispering { - w = strings.ToLower(w) + for j, r := range s { + if j > 0 && unicode.IsUpper(r) { + if cfg.allUpper || cfg.screaming { + for _, dr := range cfg.delimiter { + b.WriteRune(unicode.ToUpper(dr)) + } + } else if cfg.allLower || cfg.whispering { + for _, dr := range cfg.delimiter { + b.WriteRune(unicode.ToLower(dr)) + } + } else { + b.WriteString(cfg.delimiter) + } + } + if cfg.allUpper || cfg.screaming { + b.WriteRune(unicode.ToUpper(r)) + } else if cfg.allLower || cfg.whispering { + b.WriteRune(unicode.ToLower(r)) + } else { + b.WriteRune(r) + } + } + } else { + if cfg.allUpper || cfg.screaming { + for _, r := range s { + b.WriteRune(unicode.ToUpper(r)) + } + } else if cfg.allLower || cfg.whispering { + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } + } else { + b.WriteString(s) + } } case FirstUpperCaseWord: - w = word.String() - if cfg.mixCaseSupport { - w = splitMixCase(w, cfg.delimiter) - } + s := string(word) if cfg.allUpper || cfg.screaming { - w = strings.ToUpper(w) + for _, r := range s { + b.WriteRune(unicode.ToUpper(r)) + } } else if cfg.allLower || cfg.whispering { - w = strings.ToLower(w) + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } + } else { + first := true + for _, r := range s { + if first { + b.WriteRune(unicode.ToUpper(r)) + first = false + } else { + b.WriteRune(unicode.ToLower(r)) + } + } } case AcronymWord: - w = word.String() + s := string(word) if cfg.screaming { - w = strings.ToUpper(w) + for _, r := range s { + b.WriteRune(unicode.ToUpper(r)) + } } else if cfg.whispering { - w = strings.ToLower(w) + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } } else if cfg.caseMode == CMAllTitle { - w = UpperCaseFirst(strings.ToLower(w)) + first := true + for _, r := range s { + if first { + b.WriteRune(unicode.ToUpper(r)) + first = false + } else { + b.WriteRune(unicode.ToLower(r)) + } + } + } else { + b.WriteString(s) } case UpperCaseWord: - w = word.String() + s := string(word) if cfg.allUpper || cfg.screaming { - w = strings.ToUpper(w) + for _, r := range s { + b.WriteRune(unicode.ToUpper(r)) + } } else if cfg.allLower || cfg.whispering { - w = strings.ToLower(w) + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } } else if cfg.caseMode == CMAllTitle { - w = UpperCaseFirst(strings.ToLower(w)) + first := true + for _, r := range s { + if first { + b.WriteRune(unicode.ToUpper(r)) + first = false + } else { + b.WriteRune(unicode.ToLower(r)) + } + } } else { - w = strings.ToLower(w) + for _, r := range s { + b.WriteRune(unicode.ToLower(r)) + } } case SeparatorWord: - w = word.String() + b.WriteString(string(word)) default: - w = word.String() + b.WriteString(word.String()) } - - result = append(result, w) } - delimiter := cfg.delimiter - if cfg.upperIndicator != "" { - if cfg.upperIndicator == cfg.delimiter { - delimiter = cfg.delimiter + cfg.delimiter - } else { - delimiter = cfg.upperIndicator - } - } - final := strings.Join(result, delimiter) + final := b.String() if cfg.firstUpper { final = UpperCaseFirst(final) @@ -352,18 +455,6 @@ func separateOptionsAny(opts []any) ([]any, []any) { return parseOpts, fmtOpts } -// Helper function to split words in mixed case -func splitMixCase(input, delimiter string) string { - var result strings.Builder - result.Grow(len(input)) - for i, r := range input { - if i > 0 && unicode.IsUpper(r) { - result.WriteString(delimiter) - } - result.WriteRune(r) - } - return result.String() -} // ToKebabCase converts words into kebab-case format. func ToKebabCase(words []Word, opts ...Option) (string, error) {