Files
SkrueckenandCursor bfc1cbd4d9 Add ticket time tracking with reports and entry management.
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>
2026-10-19 11:05:10 +02:00

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)
}