// Package duration provides a partial implementation of ISO8601 durations. (no months) package duration import ( "bytes" "errors" "fmt" "regexp" "strconv" "text/template" "time" ) var ( // ErrBadFormat is returned when parsing fails ErrBadFormat = errors.New("bad format string") // ErrNoMonth is raised when a month is in the format string ErrNoMonth = errors.New("no months allowed") tmpl = template.Must(template.New("duration").Parse(`P{{if .Years}}{{.Years}}Y{{end}}{{if .Weeks}}{{.Weeks}}W{{end}}{{if .Days}}{{.Days}}D{{end}}{{if .HasTimePart}}T{{end }}{{if .Hours}}{{.Hours}}H{{end}}{{if .Minutes}}{{.Minutes}}M{{end}}{{if .Seconds}}{{.Seconds}}S{{end}}`)) full = regexp.MustCompile(`P((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?`) week = regexp.MustCompile(`P((?P\d+)W)`) ) type Duration struct { Years int Weeks int Days int Hours int Minutes int Seconds int } func FromString(dur string) (*Duration, error) { var ( match []string re *regexp.Regexp ) if week.MatchString(dur) { match = week.FindStringSubmatch(dur) re = week } else if full.MatchString(dur) { match = full.FindStringSubmatch(dur) re = full } else { return nil, ErrBadFormat } d := &Duration{} for i, name := range re.SubexpNames() { part := match[i] if i == 0 || name == "" || part == "" { continue } val, err := strconv.Atoi(part) if err != nil { return nil, err } switch name { case "year": d.Years = val case "month": return nil, ErrNoMonth case "week": d.Weeks = val case "day": d.Days = val case "hour": d.Hours = val case "minute": d.Minutes = val case "second": d.Seconds = val default: return nil, errors.New(fmt.Sprintf("unknown field %s", name)) } } return d, nil } // String prints out the value passed in. It's not strictly according to the // ISO spec, but it's pretty close. In particular, to completely conform it // would need to round up to the next largest unit. 61 seconds to 1 minute 1 // second, for example. It would also need to disallow weeks mingling with // other units. func (d *Duration) String() string { var s bytes.Buffer err := tmpl.Execute(&s, d) if err != nil { panic(err) } return s.String() } func (d *Duration) HasTimePart() bool { return d.Hours != 0 || d.Minutes != 0 || d.Seconds != 0 } func (d *Duration) ToDuration() time.Duration { day := time.Hour * 24 year := day * 365 tot := time.Duration(0) tot += year * time.Duration(d.Years) tot += day * 7 * time.Duration(d.Weeks) tot += day * time.Duration(d.Days) tot += time.Hour * time.Duration(d.Hours) tot += time.Minute * time.Duration(d.Minutes) tot += time.Second * time.Duration(d.Seconds) return tot }