package grid import ( "fmt" "time" "ticktock/internal/store" ) // Grid describes the fixed-slot day window. type Grid struct { SlotMin int // minutes per slot FirstMin int // minute-of-day at row 0, slot-aligned Rows int // number of slots } func parseHM(s string) (int, error) { t, err := time.Parse("15:04", s) if err != nil { return 0, fmt.Errorf("time %q: %w", s, err) } return t.Hour()*60 + t.Minute(), nil } func minuteOfDay(t time.Time) int { return t.Hour()*60 + t.Minute() } func floorTo(min, slot int) int { return (min / slot) * slot } func ceilTo(min, slot int) int { return ((min + slot - 1) / slot) * slot } // BuildGrid computes the slot window: [startMin,endMin] from config, expanded // outward to cover every entry's start and (if ended) end. Running entries // contribute only their start. func BuildGrid(slot, startMin, endMin int, entries []store.Entry) Grid { first, last := startMin, endMin for _, e := range entries { s := minuteOfDay(e.Start) if s < first { first = s } if !e.Running() { en := minuteOfDay(e.End) if en > last { last = en } } else if s > last { last = s } } first = floorTo(first, slot) last = ceilTo(last, slot) rows := (last - first) / slot if rows < 1 { rows = 1 } return Grid{SlotMin: slot, FirstMin: first, Rows: rows} } // SlotMinute returns the minute-of-day at the start of the given row. Passing // row == Rows yields the window end (one past the last slot). func (g Grid) SlotMinute(row int) int { return g.FirstMin + row*g.SlotMin } // RowOf returns the row containing minute, clamped to the grid. func (g Grid) RowOf(min int) int { r := (min - g.FirstMin) / g.SlotMin if min < g.FirstMin { r = 0 } if r < 0 { r = 0 } if r > g.Rows-1 { r = g.Rows - 1 } return r }