package day import ( "fmt" "regexp" "strconv" "time" ) var timeRe = regexp.MustCompile(`^(\d{1,2}):(\d{2})$`) var rangeRe = regexp.MustCompile(`^(\d{1,2}:\d{2})-(\d{1,2}:\d{2})$`) // IsTimeSyntax reports whether arg looks like a time or time range, // deciding whether a positional arg is a target or an activity name. func IsTimeSyntax(arg string) bool { return timeRe.MatchString(arg) || rangeRe.MatchString(arg) } func parseMinutes(s string) (int, error) { m := timeRe.FindStringSubmatch(s) if m == nil { return 0, fmt.Errorf("invalid time %q (want H:MM)", s) } h, err := strconv.Atoi(m[1]) if err != nil { return 0, fmt.Errorf("invalid time %q", s) } min, err := strconv.Atoi(m[2]) if err != nil { return 0, fmt.Errorf("invalid time %q", s) } if h > 23 || min > 59 { return 0, fmt.Errorf("invalid time %q", s) } return h*60 + min, nil } // ParseTarget resolves a time argument to canonical block indices. // arg "" means the slot containing now. Ranges are end-exclusive. func ParseTarget(arg string, blockSize int, now time.Time) ([]int, error) { if arg == "" { return []int{(now.Hour()*60 + now.Minute()) / blockSize}, nil } if m := rangeRe.FindStringSubmatch(arg); m != nil { start, err := parseMinutes(m[1]) if err != nil { return nil, err } end, err := parseMinutes(m[2]) if err != nil { return nil, err } if end <= start { return nil, fmt.Errorf("range end %s is not after start %s", m[2], m[1]) } first := start / blockSize last := (end - 1) / blockSize out := make([]int, 0, last-first+1) for i := first; i <= last; i++ { out = append(out, i) } return out, nil } mins, err := parseMinutes(arg) if err != nil { return nil, err } return []int{mins / blockSize}, nil } // SlotLabel renders a block index as its start time, e.g. "14:30". func SlotLabel(index, blockSize int) string { mins := index * blockSize return fmt.Sprintf("%d:%02d", mins/60, mins%60) }