diff --git a/internal/cycle/cycle.go b/internal/cycle/cycle.go index 7438f74..18e3641 100644 --- a/internal/cycle/cycle.go +++ b/internal/cycle/cycle.go @@ -29,3 +29,20 @@ func CalculateWakeTimes(sleepTime time.Time, buffer time.Duration, minCycles int } return wakeTimes } + +// CalculateCyclesInWindow returns the number of complete sleep cycles that +// fit between a sleep time and a wake time, accounting for the buffer, +// and the remaining duration after those complete cycles. +// If to is not after from, 24 hours are added to to (next-day wake). +func CalculateCyclesInWindow(from time.Time, to time.Time, buffer time.Duration) (int, time.Duration) { + if !to.After(from) { + to = to.Add(24 * time.Hour) + } + totalDuration := to.Sub(from) - buffer + if totalDuration < 0 { + return 0, 0 + } + cycles := int(totalDuration / CycleDuration) + overflow := totalDuration % CycleDuration + return cycles, overflow +} diff --git a/internal/cycle/cycle_test.go b/internal/cycle/cycle_test.go index 0a230a8..5cb002b 100644 --- a/internal/cycle/cycle_test.go +++ b/internal/cycle/cycle_test.go @@ -254,3 +254,171 @@ func TestCalculateWakeTimes_tenCycles(t *testing.T) { } } } + +func TestCalculateCyclesInWindow(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 8, 7, 0, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 6 { + t.Errorf("expected 6 cycles, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_withBuffer(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 8, 7, 15, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 15*time.Minute) + + if cycles != 6 { + t.Errorf("expected 6 cycles, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_nextDayWake(t *testing.T) { + from := time.Date(2026, time.March, 7, 23, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 7, 7, 0, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 5 { + t.Errorf("expected 5 cycles, got %d", cycles) + } + if overflow != 30*time.Minute { + t.Errorf("expected 30m overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_withOverflow(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 8, 7, 44, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 6 { + t.Errorf("expected 6 cycles, got %d", cycles) + } + if overflow != 44*time.Minute { + t.Errorf("expected 44m overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_bufferLargerThanWindow(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 7, 22, 10, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 15*time.Minute) + + if cycles != 0 { + t.Errorf("expected 0 cycles, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_overflowOnlyNoCycles(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 7, 22, 45, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 0 { + t.Errorf("expected 0 cycles, got %d", cycles) + } + if overflow != 45*time.Minute { + t.Errorf("expected 45m overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_exactlyOneCycle(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 7, 23, 30, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 1 { + t.Errorf("expected 1 cycle, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_exactlyOneCycleWithBuffer(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 7, 23, 45, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 15*time.Minute) + + if cycles != 1 { + t.Errorf("expected 1 cycle, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_zeroBuffer(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 8, 5, 30, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 5 { + t.Errorf("expected 5 cycles, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_largeBuffer(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 8, 9, 0, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 120*time.Minute) + + if cycles != 6 { + t.Errorf("expected 6 cycles, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_midnightBoundary(t *testing.T) { + from := time.Date(2026, time.March, 7, 23, 30, 0, 0, time.UTC) + to := time.Date(2026, time.March, 8, 0, 0, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 0 { + t.Errorf("expected 0 cycles, got %d", cycles) + } + if overflow != 30*time.Minute { + t.Errorf("expected 30m overflow, got %v", overflow) + } +} + +func TestCalculateCyclesInWindow_sameFromAndTo(t *testing.T) { + from := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + to := time.Date(2026, time.March, 7, 22, 0, 0, 0, time.UTC) + + cycles, overflow := CalculateCyclesInWindow(from, to, 0) + + if cycles != 16 { + t.Errorf("expected 16 cycles, got %d", cycles) + } + if overflow != 0 { + t.Errorf("expected 0 overflow, got %v", overflow) + } +}