Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions internal/cycle/cycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
168 changes: 168 additions & 0 deletions internal/cycle/cycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}