diff --git a/internal/exercises/catalog.yaml b/internal/exercises/catalog.yaml index f73ecef..8d3c7b5 100644 --- a/internal/exercises/catalog.yaml +++ b/internal/exercises/catalog.yaml @@ -197,6 +197,16 @@ concepts: hints: - Use `select` to handle multiple channel operations concurrently. - Use `time.After` to simulate a 5 microsecond timeout. +- slug: 41_time_delay + title: "Delays and Timers in Go" + difficulty: beginner + topics: ["time", "sleep", "channels", "timer"] + test_regex: ".*" + hints: + - "Use time.Sleep() to pause execution for a duration." + - "Convert milliseconds to time.Duration using time.Millisecond." + - "Use a buffered channel to send a signal after waiting." + - "Use time.After or time.Sleep to trigger events after delays." - slug: 42_wait_group title: WaitGroups test_regex: ".*" @@ -207,7 +217,7 @@ concepts: - "Call wg.Add(1) before starting a goroutine and wg.Done() when it finishes." - "Close result channels after wg.Wait() so collectors can range over them." - "Capture loop variables correctly inside goroutines (e.g., `n := n`)." - - "Return results as a slice — order does not need to match input order." + - "Return results as a slice — order does not need to match input order projects: - slug: 101_text_analyzer title: Text Analyzer (Easy) diff --git a/internal/exercises/solutions/41_time_delay/time_delay.go b/internal/exercises/solutions/41_time_delay/time_delay.go new file mode 100644 index 0000000..2a3bd58 --- /dev/null +++ b/internal/exercises/solutions/41_time_delay/time_delay.go @@ -0,0 +1,90 @@ +package timedelay + +// Real implementations using time.Now, time.Add and comparisons for the exercise. + +import ( + "errors" + "time" +) + +// WaitFor pauses execution until roughly ms milliseconds have passed. +func WaitFor(ms int) { + target := time.Now().Add(time.Duration(ms) * time.Millisecond) + for time.Now().Before(target) { + time.Sleep(1 * time.Millisecond) + } +} + +// NotifyAfter returns a channel that receives true after roughly ms milliseconds. +func NotifyAfter(ms int) chan bool { + ch := make(chan bool, 1) + target := time.Now().Add(time.Duration(ms) * time.Millisecond) + + go func() { + for time.Now().Before(target) { + time.Sleep(1 * time.Millisecond) + } + ch <- true + }() + + return ch +} + +// WaitUntil blocks until the provided target time is reached (returns immediately if in the past). +func WaitUntil(target time.Time) { + for time.Now().Before(target) { + time.Sleep(1 * time.Millisecond) + } +} + +// NotifyAt returns a channel that receives true when target time is reached (sends immediately if in the past). +func NotifyAt(target time.Time) chan bool { + ch := make(chan bool, 1) + + go func() { + for time.Now().Before(target) { + time.Sleep(1 * time.Millisecond) + } + ch <- true + }() + + return ch +} + +// ElapsedMillis returns how many milliseconds have elapsed since t. +func ElapsedMillis(t time.Time) int64 { + return time.Since(t).Milliseconds() +} + +// WaitForOrTimeout waits up to ms milliseconds but fails if total waiting exceeds timeoutMs. +func WaitForOrTimeout(ms int, timeoutMs int) error { + target := time.Now().Add(time.Duration(ms) * time.Millisecond) + deadline := time.Now().Add(time.Duration(timeoutMs) * time.Millisecond) + + for { + now := time.Now() + if !now.Before(target) { + return nil + } + if !now.Before(deadline) { + return errors.New("timeout exceeded before target reached") + } + time.Sleep(1 * time.Millisecond) + } +} + +// ScheduleAfter runs fn after roughly ms milliseconds and returns a channel closed when fn finishes. +func ScheduleAfter(ms int, fn func()) chan struct{} { + done := make(chan struct{}) + target := time.Now().Add(time.Duration(ms) * time.Millisecond) + + go func() { + for time.Now().Before(target) { + time.Sleep(1 * time.Millisecond) + } + fn() + close(done) + }() + + return done +} diff --git a/internal/exercises/templates/41_time_delay/time_delay.go b/internal/exercises/templates/41_time_delay/time_delay.go new file mode 100644 index 0000000..98a5912 --- /dev/null +++ b/internal/exercises/templates/41_time_delay/time_delay.go @@ -0,0 +1,45 @@ +package timedelay + +// Template with TODOs for teammates to implement using time.Now and time.Add. + +import "time" + +// WaitFor pauses execution until roughly ms milliseconds have passed. +func WaitForTemplate(ms int) { + // TODO: compute a target time using time.Now().Add and wait until reached +} + +// NotifyAfter waits for roughly ms milliseconds then sends true on a channel. +func NotifyAfterTemplate(ms int) chan bool { + // TODO: start a goroutine that checks time.Now against a target and sends on the channel + return nil +} + +// WaitUntil blocks until the provided target time is reached (or returns if in the past). +func WaitUntilTemplate(target time.Time) { + // TODO: loop until time.Now() is not before target +} + +// NotifyAt sends true on a channel when the provided target time is reached. +func NotifyAtTemplate(target time.Time) chan bool { + // TODO: return a channel that will receive true when the target time arrives + return nil +} + +// ElapsedMillis returns how many milliseconds have passed since t. +func ElapsedMillisTemplate(t time.Time) int64 { + // TODO: use time.Since to compute milliseconds + return 0 +} + +// WaitForOrTimeout waits for ms milliseconds but returns an error if waiting exceeds timeoutMs. +func WaitForOrTimeoutTemplate(ms int, timeoutMs int) error { + // TODO: compute target and deadline with time.Now().Add and return error on timeout + return nil +} + +// ScheduleAfter runs fn after roughly ms milliseconds and returns a channel closed when fn finishes. +func ScheduleAfterTemplate(ms int, fn func()) chan struct{} { + // TODO: start a goroutine that waits until target, runs fn, then closes the done channel + return nil +} diff --git a/internal/exercises/templates/41_time_delay/time_delay_test.go b/internal/exercises/templates/41_time_delay/time_delay_test.go new file mode 100644 index 0000000..1644220 --- /dev/null +++ b/internal/exercises/templates/41_time_delay/time_delay_test.go @@ -0,0 +1,102 @@ +package timedelay + +// Tests that validate the timing helpers with reasonable slack for CI. + +import ( + "testing" + "time" +) + +func TestWaitFor(t *testing.T) { + start := time.Now() + WaitFor(100) + el := time.Since(start).Milliseconds() + if el < 90 { + t.Fatalf("WaitFor(100) too short: %dms", el) + } + if el > 400 { + t.Fatalf("WaitFor(100) too long: %dms", el) + } +} + +func TestNotifyAfter(t *testing.T) { + start := time.Now() + ch := NotifyAfter(100) + + select { + case <-ch: + el := time.Since(start).Milliseconds() + if el < 90 { + t.Fatalf("NotifyAfter(100) too early: %dms", el) + } + if el > 500 { + t.Fatalf("NotifyAfter(100) too late: %dms", el) + } + case <-time.After(600 * time.Millisecond): + t.Fatal("NotifyAfter(100) did not send") + } +} + +func TestWaitUntilAndNotifyAt(t *testing.T) { + start := time.Now() + target := time.Now().Add(120 * time.Millisecond) + WaitUntil(target) + el := time.Since(start).Milliseconds() + if el < 100 { + t.Fatalf("WaitUntil reached too early: %dms", el) + } + if el > 600 { + t.Fatalf("WaitUntil took too long: %dms", el) + } + + start2 := time.Now() + target2 := time.Now().Add(100 * time.Millisecond) + ch := NotifyAt(target2) + select { + case <-ch: + el2 := time.Since(start2).Milliseconds() + if el2 < 80 { + t.Fatalf("NotifyAt too early: %dms", el2) + } + if el2 > 500 { + t.Fatalf("NotifyAt too late: %dms", el2) + } + case <-time.After(600 * time.Millisecond): + t.Fatal("NotifyAt did not send") + } +} + +func TestElapsedMillis(t *testing.T) { + past := time.Now().Add(-150 * time.Millisecond) + if ElapsedMillis(past) < 130 { + t.Fatalf("ElapsedMillis too small: %dms", ElapsedMillis(past)) + } +} + +func TestWaitForOrTimeout(t *testing.T) { + if err := WaitForOrTimeout(100, 300); err != nil { + t.Fatalf("expected nil, got %v", err) + } + if err := WaitForOrTimeout(300, 50); err == nil { + t.Fatal("expected timeout error, got nil") + } +} + +func TestScheduleAfter(t *testing.T) { + flag := make(chan bool, 1) + fn := func() { flag <- true } + + complete := ScheduleAfter(120, fn) + + select { + case <-flag: + case <-time.After(700 * time.Millisecond): + t.Fatal("scheduled function did not run") + } + + select { + case <-complete: + case <-time.After(700 * time.Millisecond): + t.Fatal("completion channel not closed") + } +}