diff --git a/docs/exercises.md b/docs/exercises.md
index 7a88f72..2808c4f 100644
--- a/docs/exercises.md
+++ b/docs/exercises.md
@@ -40,7 +40,7 @@ title: Exercises
20_methods - Methods
- Advanced (21-37)
+ Advanced (21-47)
- 21_interfaces - Interfaces
- 22_enums - Enums
@@ -51,6 +51,7 @@ title: Exercises
- 27_custom_errors - Custom Errors
- 36_json - JSON Processing
- 37_xml - XML Processing
+ - 47_string_functions - String Functions
diff --git a/internal/exercises/catalog.yaml b/internal/exercises/catalog.yaml
index 37b779b..d84a3f0 100644
--- a/internal/exercises/catalog.yaml
+++ b/internal/exercises/catalog.yaml
@@ -169,20 +169,19 @@ concepts:
test_regex: ".*"
hints:
- Use `make(chan T, N)` to create a buffered channel and `<-` to send and receive on it.
-- slug: 39_channel_directions
- title: Channel Directions
- test_regex: ".*"
- hints:
- - Create two channels, one for sending jobs and one for receiving results.
- - Start 5 workers using go routines.
- - Send 50 jobs to the worker pool.
- - Receive results from the worker pool and store them in a `rs` slice.
- - Enforce type safety by specifying the channel directions in worker function.
- slug: 35_channel_sync
title: Channel Synchronization
test_regex: ".*"
hints:
- Use a buffered boolean channel and wait till go routine completes.
+- slug: 36_json
+ title: JSON
+ test_regex: ".*"
+ hints:
+ - Use the encoding/json package to work with JSON data.
+ - Implement MarshalPerson to convert a struct into JSON using json.Marshal.
+ - Implement UnmarshalPerson to convert a JSON string into a struct using json.Unmarshal.
+ - Handle and return errors properly in both functions.
- slug: 37_xml
title: XML Encoding and Decoding
test_regex: ".*"
@@ -191,6 +190,33 @@ concepts:
- Add XML struct tags using `xml:"fieldname"` to map struct fields to XML elements.
- Use xml.Marshal to convert structs to XML bytes.
- Use xml.Unmarshal to parse XML bytes into structs.
+- slug: 48_sorting_by_functions
+ title: "Sorting by Functions"
+ difficulty: beginner
+ topics: ["slices", "sorting", "functions"]
+ hints:
+ - "Implement sort.Interface with Len(), Less(), and Swap() methods"
+ - "Create custom slice types like ByName and ByAge (e.g., type ByName []Person)"
+ - "Use sort.Sort() to sort slices with your custom comparison logic"
+ - "Remember to make copies of slices to avoid modifying the original"
+- slug: 38_time_formatting
+ title: "Time Formatting"
+ difficulty: beginner
+ topics: ["time", "formatting", "parsing"]
+ hints:
+ - "Use time.Now().Format() to format current time with a specific layout"
+ - "Use time.Parse() to parse time strings with known layouts"
+ - "Use time.LoadLocation() to work with different timezones"
+ - "Extract time components using .Date() and .Clock() methods"
+- slug: 39_channel_directions
+ title: Channel Directions
+ test_regex: ".*"
+ hints:
+ - Create two channels, one for sending jobs and one for receiving results.
+ - Start 5 workers using go routines.
+ - Send 50 jobs to the worker pool.
+ - Receive results from the worker pool and store them in a `rs` slice.
+ - Enforce type safety by specifying the channel directions in worker function.
- slug: 40_channel_select
title: Channel Select
test_regex: ".*"
@@ -247,7 +273,50 @@ concepts:
hints:
- Use `default` case in select to handle non-blocking channel operations.
- Safely increment `dropped` using mutex `Lock()` and `Unlock()`.
-
+- slug: 47_string_functions
+ title: String Functions
+ test_regex: ".*"
+ hints:
+ - Use strings.Contains to check if a substring exists in a string.
+ - Use strings.HasPrefix and strings.HasSuffix for prefix/suffix checking.
+ - Use strings.Index to find the position of a substring.
+ - Use strings.ToUpper and strings.ToLower for case conversion.
+ - Use strings.TrimSpace to remove leading and trailing whitespace.
+- slug: 49_panic
+ title: Panic and Recover
+ test_regex: ".*"
+ hints:
+ - "Use `panic()` to simulate runtime errors when appropriate."
+ - "Use `defer` with `recover()` to catch and handle panics."
+ - "Recovering from panics allows graceful handling of unexpected situations."
+ - "Remember: `recover()` only works inside a deferred function."
+- slug: 50_timers
+ title: "Timers"
+ difficulty: medium
+ topics: ["time", "goroutines", "concurrency", "synchronization"]
+ test_regex: ".*"
+ hints:
+ - "Use a map[string]*time.Timer to track active timers by key."
+ - "Use a mutex to safely access the timers map concurrently."
+ - "Start a timer with time.AfterFunc(d, fn) to run a callback after duration d."
+ - "If Start is called again for an existing key, stop and replace the old timer."
+ - "Stop should remove the timer from the map and prevent the callback from firing."
+ - "Reset can call timer.Reset(d) to reschedule the same callback."
+ - "Remember to remove timers from the map after they fire to avoid memory leaks."
+ - "Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios."
+- slug: 51_rate_limiting
+ title: Rate Limiting
+ test_regex: ".*"
+ difficulty: medium
+ topics: ["time", "concurrency", "maps"]
+ hints:
+ - "Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time."
+ - "Use a mutex to safely handle concurrent access to the map."
+ - "In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit."
+ - "Reset should clear the request history for a key; if the key does not exist, do nothing."
+ - "Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary."
+ - "In tests, use time.Sleep with a small buffer above the interval to avoid flakiness."
+ - "Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors."
projects:
- slug: 101_text_analyzer
title: Text Analyzer (Easy)
@@ -289,15 +358,6 @@ projects:
test_regex: ".*"
hints:
- Implement an in-memory key-value store with basic CRUD operations and optional persistence.
-- slug: 36_json
- title: JSON
- test_regex: ".*"
- hints:
- - Use the encoding/json package to work with JSON data.
- - Implement MarshalPerson to convert a struct into JSON using json.Marshal.
- - Implement UnmarshalPerson to convert a JSON string into a struct using json.Unmarshal.
- - Handle and return errors properly in both functions.
-
- slug: 109_epoch
title: "Epoch Conversion"
difficulty: beginner
@@ -305,62 +365,4 @@ projects:
hints:
- "Use Go's `time.Unix()` to convert an epoch to time."
- "Use `t.Unix()` to convert time back to epoch."
- - "Remember Go’s `time.Parse` can help parse date strings."
-
-- slug: 37_sorting_by_functions
- title: "Sorting by Functions"
- difficulty: beginner
- topics: ["slices", "sorting", "functions"]
- hints:
- - "Implement sort.Interface with Len(), Less(), and Swap() methods"
- - "Create custom slice types like ByName and ByAge (e.g., type ByName []Person)"
- - "Use sort.Sort() to sort slices with your custom comparison logic"
- - "Remember to make copies of slices to avoid modifying the original"
-
-- slug: 38_time_formatting
- title: "Time Formatting"
- difficulty: beginner
- topics: ["time", "formatting", "parsing"]
- hints:
- - "Use time.Now().Format() to format current time with a specific layout"
- - "Use time.Parse() to parse time strings with known layouts"
- - "Use time.LoadLocation() to work with different timezones"
- - "Extract time components using .Date() and .Clock() methods"
-
-- slug: 39_panic
- title: Panic and Recover
- test_regex: ".*"
- hints:
- - "Use `panic()` to simulate runtime errors when appropriate."
- - "Use `defer` with `recover()` to catch and handle panics."
- - "Recovering from panics allows graceful handling of unexpected situations."
- - "Remember: `recover()` only works inside a deferred function."
-
-- slug: 64_timers
- title: "Timers"
- difficulty: medium
- topics: ["time", "goroutines", "concurrency", "synchronization"]
- test_regex: ".*"
- hints:
- - "Use a map[string]*time.Timer to track active timers by key."
- - "Use a mutex to safely access the timers map concurrently."
- - "Start a timer with time.AfterFunc(d, fn) to run a callback after duration d."
- - "If Start is called again for an existing key, stop and replace the old timer."
- - "Stop should remove the timer from the map and prevent the callback from firing."
- - "Reset can call timer.Reset(d) to reschedule the same callback."
- - "Remember to remove timers from the map after they fire to avoid memory leaks."
- - "Write tests to verify Start, Stop, and Reset behavior, including concurrency scenarios."
-
-- slug: 68_rate_limiting
- title: Rate Limiting
- test_regex: ".*"
- difficulty: medium
- topics: ["time", "concurrency", "maps"]
- hints:
- - "Implement a RateLimiter struct that tracks request timestamps per key using a map[string][]time.Time."
- - "Use a mutex to safely handle concurrent access to the map."
- - "In the Allow method, remove timestamps older than the interval and check if the number of recent requests exceeds the limit."
- - "Reset should clear the request history for a key; if the key does not exist, do nothing."
- - "Consider edge cases: multiple keys, concurrent access, and requests exactly at the interval boundary."
- - "In tests, use time.Sleep with a small buffer above the interval to avoid flakiness."
- - "Initialize the timestamps map in NewRateLimiter to prevent nil pointer errors."
+ - "Remember Go's `time.Parse` can help parse date strings."
diff --git a/internal/exercises/solutions/47_string_functions/string_functions.go b/internal/exercises/solutions/47_string_functions/string_functions.go
new file mode 100644
index 0000000..cdc2588
--- /dev/null
+++ b/internal/exercises/solutions/47_string_functions/string_functions.go
@@ -0,0 +1,38 @@
+package string_functions
+
+import "strings"
+
+// Contains checks if substr is within s
+func Contains(s, substr string) bool {
+ return strings.Contains(s, substr)
+}
+
+// HasPrefix tests whether the string s begins with prefix
+func HasPrefix(s, prefix string) bool {
+ return strings.HasPrefix(s, prefix)
+}
+
+// HasSuffix tests whether the string s ends with suffix
+func HasSuffix(s, suffix string) bool {
+ return strings.HasSuffix(s, suffix)
+}
+
+// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s
+func Index(s, substr string) int {
+ return strings.Index(s, substr)
+}
+
+// ToUpper returns s with all Unicode letters mapped to their upper case
+func ToUpper(s string) string {
+ return strings.ToUpper(s)
+}
+
+// ToLower returns s with all Unicode letters mapped to their lower case
+func ToLower(s string) string {
+ return strings.ToLower(s)
+}
+
+// TrimSpace returns s with all leading and trailing white space removed
+func TrimSpace(s string) string {
+ return strings.TrimSpace(s)
+}
diff --git a/internal/exercises/templates/47_string_functions/string_functions.go b/internal/exercises/templates/47_string_functions/string_functions.go
new file mode 100644
index 0000000..f8c0d60
--- /dev/null
+++ b/internal/exercises/templates/47_string_functions/string_functions.go
@@ -0,0 +1,46 @@
+package string_functions
+
+// TODO: Implement these functions using Go's strings package
+// You'll need to import "strings" package when implementing the functions
+
+// Contains checks if substr is within s
+func Contains(s, substr string) bool {
+ // TODO: use strings.Contains
+ return false // Intentionally wrong to simulate failing exercise
+}
+
+// HasPrefix tests whether the string s begins with prefix
+func HasPrefix(s, prefix string) bool {
+ // TODO: use strings.HasPrefix
+ return false // Intentionally wrong to simulate failing exercise
+}
+
+// HasSuffix tests whether the string s ends with suffix
+func HasSuffix(s, suffix string) bool {
+ // TODO: use strings.HasSuffix
+ return false // Intentionally wrong to simulate failing exercise
+}
+
+// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s
+func Index(s, substr string) int {
+ // TODO: use strings.Index
+ return -1 // Intentionally wrong to simulate failing exercise
+}
+
+// ToUpper returns s with all Unicode letters mapped to their upper case
+func ToUpper(s string) string {
+ // TODO: use strings.ToUpper
+ return "" // Intentionally wrong to simulate failing exercise
+}
+
+// ToLower returns s with all Unicode letters mapped to their lower case
+func ToLower(s string) string {
+ // TODO: use strings.ToLower
+ return "" // Intentionally wrong to simulate failing exercise
+}
+
+// TrimSpace returns s with all leading and trailing white space removed
+func TrimSpace(s string) string {
+ // TODO: use strings.TrimSpace
+ return "" // Intentionally wrong to simulate failing exercise
+}
diff --git a/internal/exercises/templates/47_string_functions/string_functions_test.go b/internal/exercises/templates/47_string_functions/string_functions_test.go
new file mode 100644
index 0000000..9550c31
--- /dev/null
+++ b/internal/exercises/templates/47_string_functions/string_functions_test.go
@@ -0,0 +1,153 @@
+package string_functions
+
+import "testing"
+
+func TestContains(t *testing.T) {
+ tests := []struct {
+ s string
+ substr string
+ want bool
+ }{
+ {"hello world", "world", true},
+ {"hello world", "hello", true},
+ {"hello world", "foo", false},
+ {"", "", true},
+ {"hello", "", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.s+" contains "+tt.substr, func(t *testing.T) {
+ if got := Contains(tt.s, tt.substr); got != tt.want {
+ t.Errorf("Contains(%q, %q) = %v, want %v", tt.s, tt.substr, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestHasPrefix(t *testing.T) {
+ tests := []struct {
+ s string
+ prefix string
+ want bool
+ }{
+ {"hello world", "hello", true},
+ {"hello world", "world", false},
+ {"", "", true},
+ {"hello", "hello", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.s+" has prefix "+tt.prefix, func(t *testing.T) {
+ if got := HasPrefix(tt.s, tt.prefix); got != tt.want {
+ t.Errorf("HasPrefix(%q, %q) = %v, want %v", tt.s, tt.prefix, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestHasSuffix(t *testing.T) {
+ tests := []struct {
+ s string
+ suffix string
+ want bool
+ }{
+ {"hello world", "world", true},
+ {"hello world", "hello", false},
+ {"", "", true},
+ {"hello", "hello", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.s+" has suffix "+tt.suffix, func(t *testing.T) {
+ if got := HasSuffix(tt.s, tt.suffix); got != tt.want {
+ t.Errorf("HasSuffix(%q, %q) = %v, want %v", tt.s, tt.suffix, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestIndex(t *testing.T) {
+ tests := []struct {
+ s string
+ substr string
+ want int
+ }{
+ {"hello world", "world", 6},
+ {"hello world", "hello", 0},
+ {"hello world", "foo", -1},
+ {"", "", 0},
+ {"hello", "l", 2},
+ }
+
+ for _, tt := range tests {
+ t.Run("index of "+tt.substr+" in "+tt.s, func(t *testing.T) {
+ if got := Index(tt.s, tt.substr); got != tt.want {
+ t.Errorf("Index(%q, %q) = %v, want %v", tt.s, tt.substr, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestToUpper(t *testing.T) {
+ tests := []struct {
+ s string
+ want string
+ }{
+ {"hello", "HELLO"},
+ {"Hello World", "HELLO WORLD"},
+ {"", ""},
+ {"123", "123"},
+ {"héllo", "HÉLLO"},
+ }
+
+ for _, tt := range tests {
+ t.Run("ToUpper("+tt.s+")", func(t *testing.T) {
+ if got := ToUpper(tt.s); got != tt.want {
+ t.Errorf("ToUpper(%q) = %q, want %q", tt.s, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestToLower(t *testing.T) {
+ tests := []struct {
+ s string
+ want string
+ }{
+ {"HELLO", "hello"},
+ {"Hello World", "hello world"},
+ {"", ""},
+ {"123", "123"},
+ {"HÉLLO", "héllo"},
+ }
+
+ for _, tt := range tests {
+ t.Run("ToLower("+tt.s+")", func(t *testing.T) {
+ if got := ToLower(tt.s); got != tt.want {
+ t.Errorf("ToLower(%q) = %q, want %q", tt.s, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestTrimSpace(t *testing.T) {
+ tests := []struct {
+ s string
+ want string
+ }{
+ {" hello ", "hello"},
+ {" hello world ", "hello world"},
+ {"", ""},
+ {"hello", "hello"},
+ {" ", ""},
+ {"\n\thello\t\n", "hello"},
+ }
+
+ for _, tt := range tests {
+ t.Run("TrimSpace("+tt.s+")", func(t *testing.T) {
+ if got := TrimSpace(tt.s); got != tt.want {
+ t.Errorf("TrimSpace(%q) = %q, want %q", tt.s, got, tt.want)
+ }
+ })
+ }
+}