package http import ( "fmt" "strconv" "time" "github.com/influxdata/influxdb/v2" ) // ErrInvalidDuration is returned when parsing a malformatted duration. var ErrInvalidDuration = &influxdb.Error{ Code: influxdb.EInvalid, Msg: "invalid duration", } // ParseDuration parses a time duration from a string. // This is needed instead of time.ParseDuration because this will support // the full syntax that InfluxQL supports for specifying durations // including weeks and days. func ParseDuration(s string) (time.Duration, error) { // Return an error if the string is blank or one character if len(s) < 2 { return 0, ErrInvalidDuration } // Split string into individual runes. a := split(s) // Start with a zero duration. var d time.Duration i := 0 // Check for a negative. isNegative := false if a[i] == '-' { isNegative = true i++ } var measure int64 var unit string // Parsing loop. for i < len(a) { // Find the number portion. start := i for ; i < len(a) && isDigit(a[i]); i++ { // Scan for the digits. } // Check if we reached the end of the string prematurely. if i >= len(a) || i == start { return 0, ErrInvalidDuration } // Parse the numeric part. n, err := strconv.ParseInt(string(a[start:i]), 10, 64) if err != nil { return 0, ErrInvalidDuration } measure = n // Extract the unit of measure. unit = string(a[i]) switch a[i] { case 'n': d += time.Duration(n) * time.Nanosecond if i+1 < len(a) && a[i+1] == 's' { unit = string(a[i : i+2]) i += 2 continue } case 'u', 'ยต': d += time.Duration(n) * time.Microsecond if i+1 < len(a) && a[i+1] == 's' { unit = string(a[i : i+2]) i += 2 continue } case 'm': // Check for `mo` and `ms`; month and millisecond, respectively. if i+1 < len(a) { switch a[i+1] { case 's': // ms == milliseconds unit = string(a[i : i+2]) d += time.Duration(n) * time.Millisecond i += 2 continue case 'o': // mo == month // TODO(goller): use real duration values: // https://github.com/influxdata/platform/issues/657 unit = string(a[i : i+2]) d += time.Duration(n) * 30 * 24 * time.Hour i += 2 continue } } d += time.Duration(n) * time.Minute case 's': d += time.Duration(n) * time.Second case 'h': d += time.Duration(n) * time.Hour case 'd': d += time.Duration(n) * 24 * time.Hour case 'w': // TODO(goller): use real duration values: // https://github.com/influxdata/platform/issues/657 d += time.Duration(n) * 7 * 24 * time.Hour case 'y': // TODO(goller): use real duration values: // https://github.com/influxdata/platform/issues/657 d += time.Duration(n) * 365 * 24 * time.Hour default: return 0, ErrInvalidDuration } i++ } // Check to see if we overflowed a duration if d < 0 && !isNegative { return 0, fmt.Errorf("overflowed duration %d%s: choose a smaller duration or INF", measure, unit) } if isNegative { d = -d } return d, nil } // FormatDuration formats a duration to a string. func FormatDuration(d time.Duration) string { if d == 0 { return "0s" } else if d%(365*24*time.Hour) == 0 { return fmt.Sprintf("%dy", d/(365*24*time.Hour)) } else if d%(30*24*time.Hour) == 0 { return fmt.Sprintf("%dmo", d/(30*24*time.Hour)) } else if d%(7*24*time.Hour) == 0 { return fmt.Sprintf("%dw", d/(7*24*time.Hour)) } else if d%(24*time.Hour) == 0 { return fmt.Sprintf("%dd", d/(24*time.Hour)) } else if d%time.Hour == 0 { return fmt.Sprintf("%dh", d/time.Hour) } else if d%time.Minute == 0 { return fmt.Sprintf("%dm", d/time.Minute) } else if d%time.Second == 0 { return fmt.Sprintf("%ds", d/time.Second) } else if d%time.Millisecond == 0 { return fmt.Sprintf("%dms", d/time.Millisecond) } else if d%time.Microsecond == 0 { return fmt.Sprintf("%dus", d/time.Microsecond) } return fmt.Sprintf("%dns", d/time.Nanosecond) } // split splits a string into a slice of runes. func split(s string) (a []rune) { for _, ch := range s { a = append(a, ch) } return } // isDigit returns true if the rune is a digit. func isDigit(ch rune) bool { return (ch >= '0' && ch <= '9') }