String Builder

Exercise: String Builder

Difficulty - Beginner

Learning Objectives

  • Master string manipulation in Go
  • Understand the efficiency of strings.Builder
  • Practice string processing algorithms
  • Learn to avoid common string performance pitfalls

Problem Statement

Create a StringUtils package with efficient string manipulation functions:

  1. WordCount: Count the number of words in a string
  2. Reverse: Reverse a string
  3. IsPalindrome: Check if a string reads the same forwards and backwards
  4. ToCamelCase: Convert "snake_case" or "kebab-case" to "camelCase"
  5. Truncate: Truncate a string to a maximum length with ellipsis

Function Signatures

 1package stringutils
 2
 3// WordCount returns the number of words in the string
 4func WordCount(s string) int
 5
 6// Reverse returns the reversed string
 7func Reverse(s string) string
 8
 9// IsPalindrome checks if a string is a palindrome
10func IsPalindrome(s string) bool
11
12// ToCamelCase converts snake_case or kebab-case to camelCase
13func ToCamelCase(s string) string
14
15// Truncate shortens a string to maxLen, adding "..." if truncated
16func Truncate(s string, maxLen int) string

Example Usage

 1package main
 2
 3import (
 4    "fmt"
 5    "stringutils"
 6)
 7
 8func main() {
 9    // WordCount
10    text := "Hello, world! How are you?"
11    count := stringutils.WordCount(text)
12    fmt.Println(count) // 5
13
14    // Reverse
15    reversed := stringutils.Reverse("Hello, 世界")
16    fmt.Println(reversed) // 界世 ,olleH
17
18    // IsPalindrome
19    fmt.Println(stringutils.IsPalindrome("racecar"))    // true
20    fmt.Println(stringutils.IsPalindrome("hello"))      // false
21
22    // ToCamelCase
23    snake := "user_name_field"
24    camel := stringutils.ToCamelCase(snake)
25    fmt.Println(camel) // userNameField
26
27    kebab := "user-name-field"
28    camel2 := stringutils.ToCamelCase(kebab)
29    fmt.Println(camel2) // userNameField
30
31    // Truncate
32    long := "This is a very long string that needs to be truncated"
33    short := stringutils.Truncate(long, 20)
34    fmt.Println(short) // This is a very lo...
35}

Requirements

  1. WordCount should handle multiple consecutive spaces
  2. Reverse must properly handle Unicode characters
  3. IsPalindrome should be case-insensitive and ignore spaces/punctuation
  4. ToCamelCase should handle both snake_case and kebab-case
  5. Truncate should add "..." only if the string was actually truncated

Test Cases

  1package stringutils
  2
  3import "testing"
  4
  5func TestWordCount(t *testing.T) {
  6    tests := []struct {
  7        input    string
  8        expected int
  9    }{
 10        {"Hello world", 2},
 11        {"Hello   world", 2}, // Multiple spaces
 12        {"", 0},
 13        {"   ", 0},
 14        {"One", 1},
 15        {"One two three four five", 5},
 16    }
 17
 18    for _, tt := range tests {
 19        result := WordCount(tt.input)
 20        if result != tt.expected {
 21            t.Errorf("WordCount(%q) = %d; want %d", tt.input, result, tt.expected)
 22        }
 23    }
 24}
 25
 26func TestReverse(t *testing.T) {
 27    tests := []struct {
 28        input    string
 29        expected string
 30    }{
 31        {"hello", "olleh"},
 32        {"Hello, 世界", "界世 ,olleH"},
 33        {"", ""},
 34        {"a", "a"},
 35        {"Go", "oG"},
 36    }
 37
 38    for _, tt := range tests {
 39        result := Reverse(tt.input)
 40        if result != tt.expected {
 41            t.Errorf("Reverse(%q) = %q; want %q", tt.input, result, tt.expected)
 42        }
 43    }
 44}
 45
 46func TestIsPalindrome(t *testing.T) {
 47    tests := []struct {
 48        input    string
 49        expected bool
 50    }{
 51        {"racecar", true},
 52        {"RaceCar", true},
 53        {"A man a plan a canal Panama", true},
 54        {"hello", false},
 55        {"", true},
 56        {"a", true},
 57    }
 58
 59    for _, tt := range tests {
 60        result := IsPalindrome(tt.input)
 61        if result != tt.expected {
 62            t.Errorf("IsPalindrome(%q) = %v; want %v", tt.input, result, tt.expected)
 63        }
 64    }
 65}
 66
 67func TestToCamelCase(t *testing.T) {
 68    tests := []struct {
 69        input    string
 70        expected string
 71    }{
 72        {"user_name", "userName"},
 73        {"user-name", "userName"},
 74        {"user_name_field", "userNameField"},
 75        {"already_camel", "alreadyCamel"},
 76        {"", ""},
 77        {"single", "single"},
 78    }
 79
 80    for _, tt := range tests {
 81        result := ToCamelCase(tt.input)
 82        if result != tt.expected {
 83            t.Errorf("ToCamelCase(%q) = %q; want %q", tt.input, result, tt.expected)
 84        }
 85    }
 86}
 87
 88func TestTruncate(t *testing.T) {
 89    tests := []struct {
 90        input    string
 91        maxLen   int
 92        expected string
 93    }{
 94        {"Hello, world!", 10, "Hello, ..."},
 95        {"Short", 10, "Short"},
 96        {"Exactly10!", 10, "Exactly10!"},
 97        {"", 5, ""},
 98        {"Hi", 10, "Hi"},
 99    }
100
101    for _, tt := range tests {
102        result := Truncate(tt.input, tt.maxLen)
103        if result != tt.expected {
104            t.Errorf("Truncate(%q, %d) = %q; want %q", tt.input, tt.maxLen, result, tt.expected)
105        }
106    }
107}

Hints

Hint 1: WordCount

Use strings.Fields() which splits on whitespace and handles multiple spaces automatically.

Hint 2: Reverse

Convert the string to a slice of runes, reverse the slice, then convert back to a string.

1runes := []rune(s)
2// reverse runes slice
3return string(runes)
Hint 3: IsPalindrome

Convert to lowercase and remove non-alphanumeric characters. Then compare with its reverse.

Hint 4: ToCamelCase

Split on '_' or '-', capitalize the first letter of each word except the first, then join.

Hint 5: Truncate

Check if the string length exceeds maxLen. If so, take substring up to and append "...".


Solution

Click to see the complete solution
 1package stringutils
 2
 3import (
 4    "strings"
 5    "unicode"
 6)
 7
 8// WordCount returns the number of words in the string
 9func WordCount(s string) int {
10    // Fields splits on whitespace and handles multiple spaces
11    fields := strings.Fields(s)
12    return len(fields)
13}
14
15// Reverse returns the reversed string
16func Reverse(s string) string {
17    // Convert to runes to handle Unicode properly
18    runes := []rune(s)
19
20    // Reverse the rune slice
21    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
22        runes[i], runes[j] = runes[j], runes[i]
23    }
24
25    return string(runes)
26}
27
28// IsPalindrome checks if a string is a palindrome
29func IsPalindrome(s string) bool {
30    // Normalize: lowercase and keep only alphanumeric
31    var normalized strings.Builder
32    for _, r := range s {
33        if unicode.IsLetter(r) || unicode.IsDigit(r) {
34            normalized.WriteRune(unicode.ToLower(r))
35        }
36    }
37
38    cleaned := normalized.String()
39
40    // Compare with reverse
41    return cleaned == Reverse(cleaned)
42}
43
44// ToCamelCase converts snake_case or kebab-case to camelCase
45func ToCamelCase(s string) string {
46    if s == "" {
47        return ""
48    }
49
50    // Replace hyphens with underscores for uniform handling
51    s = strings.ReplaceAll(s, "-", "_")
52
53    // Split on underscores
54    parts := strings.Split(s, "_")
55
56    var result strings.Builder
57    for i, part := range parts {
58        if part == "" {
59            continue
60        }
61
62        if i == 0 {
63            // First word: keep lowercase
64            result.WriteString(strings.ToLower(part))
65        } else {
66            // Subsequent words: capitalize first letter
67            result.WriteString(strings.Title(strings.ToLower(part)))
68        }
69    }
70
71    return result.String()
72}
73
74// Truncate shortens a string to maxLen, adding "..." if truncated
75func Truncate(s string, maxLen int) string {
76    if len(s) <= maxLen {
77        return s
78    }
79
80    if maxLen <= 3 {
81        return s[:maxLen]
82    }
83
84    return s[:maxLen-3] + "..."
85}

Explanation

WordCount:

  • Uses strings.Fields() which automatically handles multiple spaces
  • Returns 0 for empty or whitespace-only strings
  • Simple and efficient O(n) solution

Reverse:

  • Converts string to []rune to handle Unicode correctly
  • Uses two-pointer technique for in-place reversal
  • Converting to runes is essential for proper Unicode handling
  • Example: "Hello, 世界" has 9 characters but only 8 runes

IsPalindrome:

  • Normalizes by removing non-alphanumeric characters and converting to lowercase
  • Uses strings.Builder for efficient string construction
  • Compares normalized string with its reverse
  • Case-insensitive and ignores punctuation/spaces

ToCamelCase:

  • Handles both snake_case and kebab-case by normalizing to underscores
  • Splits on underscores to get word parts
  • First word remains lowercase, subsequent words are title-cased
  • Uses strings.Builder for efficient concatenation

Truncate:

  • Simple length check to avoid unnecessary work
  • Handles edge case where maxLen <= 3
  • Subtracts 3 from maxLen to account for "..." suffix
  • Only adds "..." if string was actually truncated

Performance Considerations

Avoid String Concatenation in Loops:

 1// Bad: Creates new string on each iteration
 2func badConcat(words []string) string {
 3    result := ""
 4    for _, word := range words {
 5        result += word // O(n²) complexity
 6    }
 7    return result
 8}
 9
10// Good: Use strings.Builder
11func goodConcat(words []string) string {
12    var builder strings.Builder
13    for _, word := range words {
14        builder.WriteString(word) // O(n) complexity
15    }
16    return builder.String()
17}

Runes vs Bytes:

 1// For ASCII-only strings, byte operations are faster
 2func reverseASCII(s string) string {
 3    bytes := []byte(s)
 4    for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
 5        bytes[i], bytes[j] = bytes[j], bytes[i]
 6    }
 7    return string(bytes)
 8}
 9
10// But for Unicode, must use runes
11func reverseUnicode(s string) string {
12    runes := []rune(s)
13    // ... reverse logic
14    return string(runes)
15}

Bonus Challenges

  1. Word Wrap: Implement a function that wraps text at a given line width
1func WordWrap(text string, width int) string
  1. Levenshtein Distance: Calculate edit distance between two strings
1func LevenshteinDistance(s1, s2 string) int
  1. Template Engine: Simple string template with variable substitution
1func Template(template string, vars map[string]string) string
2// Example: Template("Hello, {{name}}!", map[string]string{"name": "World"})
3// Returns: "Hello, World!"
  1. String Compression: Implement run-length encoding
1func Compress(s string) string
2// "aaabbbccc" -> "a3b3c3"

Key Takeaways

  • Use strings.Builder for efficient string concatenation in loops
  • String are immutable in Go - operations create new strings
  • Handle Unicode properly by converting to []rune when necessary
  • strings package provides many useful functions - learn them
  • Performance matters - string operations can be expensive at scale

String manipulation is fundamental in programming. Go's strings package is powerful, but understanding when to use []byte vs []rune and when to use strings.Builder vs simple concatenation is crucial for writing efficient code.