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