Support start/stop timer, manual durations, grouped reports, and per-ticket edit/delete of time entries with undo. Co-authored-by: Cursor <cursoragent@cursor.com>
60 lines
1.8 KiB
Go
60 lines
1.8 KiB
Go
package ticket
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
reHourMin = regexp.MustCompile(`(?i)^(\d+)\s*h(?:ours?)?\s*(\d+)\s*m(?:ins?)?$`)
|
|
reHourOnly = regexp.MustCompile(`(?i)^(\d+(?:\.\d+)?)\s*h(?:ours?)?$`)
|
|
reMinOnly = regexp.MustCompile(`(?i)^(\d+)\s*m(?:ins?)?$`)
|
|
reSecOnly = regexp.MustCompile(`(?i)^(\d+)\s*s(?:ecs?)?$`)
|
|
reColon = regexp.MustCompile(`^(\d+):(\d{1,2})$`)
|
|
)
|
|
|
|
// ParseDurationFlexible parses user durations like 1h30m, 90m, 1.5h, 1:30.
|
|
func ParseDurationFlexible(s string) (time.Duration, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0, fmt.Errorf("duration required")
|
|
}
|
|
// Prefer Go's parser when it works (e.g. 1h30m, 90m).
|
|
if d, err := time.ParseDuration(strings.ToLower(s)); err == nil && d > 0 {
|
|
return d, nil
|
|
}
|
|
|
|
if m := reHourMin.FindStringSubmatch(s); m != nil {
|
|
h, _ := strconv.Atoi(m[1])
|
|
min, _ := strconv.Atoi(m[2])
|
|
return time.Duration(h)*time.Hour + time.Duration(min)*time.Minute, nil
|
|
}
|
|
if m := reHourOnly.FindStringSubmatch(s); m != nil {
|
|
h, err := strconv.ParseFloat(m[1], 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid duration %q", s)
|
|
}
|
|
return time.Duration(h * float64(time.Hour)), nil
|
|
}
|
|
if m := reMinOnly.FindStringSubmatch(s); m != nil {
|
|
min, _ := strconv.Atoi(m[1])
|
|
return time.Duration(min) * time.Minute, nil
|
|
}
|
|
if m := reSecOnly.FindStringSubmatch(s); m != nil {
|
|
sec, _ := strconv.Atoi(m[1])
|
|
return time.Duration(sec) * time.Second, nil
|
|
}
|
|
if m := reColon.FindStringSubmatch(s); m != nil {
|
|
h, _ := strconv.Atoi(m[1])
|
|
min, _ := strconv.Atoi(m[2])
|
|
if min >= 60 {
|
|
return 0, fmt.Errorf("invalid minutes in %q", s)
|
|
}
|
|
return time.Duration(h)*time.Hour + time.Duration(min)*time.Minute, nil
|
|
}
|
|
return 0, fmt.Errorf("invalid duration %q (try 1h30m, 90m, 1.5h)", s)
|
|
}
|