diff --git a/model/time.go b/model/time.go index 5727452c1..fed9e87b9 100644 --- a/model/time.go +++ b/model/time.go @@ -201,6 +201,7 @@ var unitMap = map[string]struct { // ParseDuration parses a string into a time.Duration, assuming that a year // always has 365d, a week always has 7d, and a day always has 24h. +// Negative durations are not supported. func ParseDuration(s string) (Duration, error) { switch s { case "0": @@ -253,18 +254,36 @@ func ParseDuration(s string) (Duration, error) { return 0, errors.New("duration out of range") } } + return Duration(dur), nil } +// ParseDurationAllowNegative is like ParseDuration but also accepts negative durations. +func ParseDurationAllowNegative(s string) (Duration, error) { + if s == "" || s[0] != '-' { + return ParseDuration(s) + } + + d, err := ParseDuration(s[1:]) + + return -d, err +} + func (d Duration) String() string { var ( - ms = int64(time.Duration(d) / time.Millisecond) - r = "" + ms = int64(time.Duration(d) / time.Millisecond) + r = "" + sign = "" ) + if ms == 0 { return "0s" } + if ms < 0 { + sign, ms = "-", -ms + } + f := func(unit string, mult int64, exact bool) { if exact && ms%mult != 0 { return @@ -286,7 +305,7 @@ func (d Duration) String() string { f("s", 1000, false) f("ms", 1, false) - return r + return sign + r } // MarshalJSON implements the json.Marshaler interface. diff --git a/model/time_test.go b/model/time_test.go index 70f383947..a4e9069f1 100644 --- a/model/time_test.go +++ b/model/time_test.go @@ -68,70 +68,171 @@ func TestDuration(t *testing.T) { } func TestParseDuration(t *testing.T) { - cases := []struct { - in string - out time.Duration + type testCase struct { + in string + out time.Duration + expectedString string + allowedNegative bool + } - expectedString string - }{ + baseCases := []testCase{ { - in: "0", - out: 0, - expectedString: "0s", - }, { - in: "0w", - out: 0, - expectedString: "0s", - }, { - in: "0s", - out: 0, - }, { - in: "324ms", - out: 324 * time.Millisecond, - }, { - in: "3s", - out: 3 * time.Second, - }, { - in: "5m", - out: 5 * time.Minute, - }, { - in: "1h", - out: time.Hour, - }, { - in: "4d", - out: 4 * 24 * time.Hour, - }, { - in: "4d1h", - out: 4*24*time.Hour + time.Hour, - }, { - in: "14d", - out: 14 * 24 * time.Hour, - expectedString: "2w", - }, { - in: "3w", - out: 3 * 7 * 24 * time.Hour, - }, { - in: "3w2d1h", - out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, - expectedString: "23d1h", - }, { - in: "10y", - out: 10 * 365 * 24 * time.Hour, + in: "0", + out: 0, + expectedString: "0s", + allowedNegative: false, + }, + { + in: "0w", + out: 0, + expectedString: "0s", + allowedNegative: false, + }, + { + in: "0s", + out: 0, + expectedString: "", + allowedNegative: false, + }, + { + in: "324ms", + out: 324 * time.Millisecond, + expectedString: "", + allowedNegative: false, + }, + { + in: "3s", + out: 3 * time.Second, + expectedString: "", + allowedNegative: false, + }, + { + in: "5m", + out: 5 * time.Minute, + expectedString: "", + allowedNegative: false, + }, + { + in: "1h", + out: time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "4d", + out: 4 * 24 * time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "4d1h", + out: 4*24*time.Hour + time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "14d", + out: 14 * 24 * time.Hour, + expectedString: "2w", + allowedNegative: false, + }, + { + in: "3w", + out: 3 * 7 * 24 * time.Hour, + expectedString: "", + allowedNegative: false, + }, + { + in: "3w2d1h", + out: 3*7*24*time.Hour + 2*24*time.Hour + time.Hour, + expectedString: "23d1h", + allowedNegative: false, + }, + { + in: "10y", + out: 10 * 365 * 24 * time.Hour, + expectedString: "", + allowedNegative: false, }, } - for _, c := range cases { - d, err := ParseDuration(c.in) + negativeCases := []testCase{ + { + in: "-3s", + out: -3 * time.Second, + expectedString: "", + allowedNegative: true, + }, + { + in: "-5m", + out: -5 * time.Minute, + expectedString: "", + allowedNegative: true, + }, + { + in: "-1h", + out: -1 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + { + in: "-2d", + out: -2 * 24 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + { + in: "-1w", + out: -7 * 24 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + { + in: "-3w2d1h", + out: -(3*7*24*time.Hour + 2*24*time.Hour + time.Hour), + expectedString: "-23d1h", + allowedNegative: true, + }, + { + in: "-10y", + out: -10 * 365 * 24 * time.Hour, + expectedString: "", + allowedNegative: true, + }, + } + + for _, c := range baseCases { + c.allowedNegative = true + negativeCases = append(negativeCases, c) + } + + allCases := append(baseCases, negativeCases...) + + for _, c := range allCases { + var ( + d Duration + err error + ) + + if c.allowedNegative { + d, err = ParseDurationAllowNegative(c.in) + } else { + d, err = ParseDuration(c.in) + } + if err != nil { t.Errorf("Unexpected error on input %q", c.in) } + if time.Duration(d) != c.out { t.Errorf("Expected %v but got %v", c.out, d) } + expectedString := c.expectedString - if c.expectedString == "" { + if expectedString == "" { expectedString = c.in } + if d.String() != expectedString { t.Errorf("Expected duration string %q but got %q", c.in, d.String()) } @@ -307,7 +408,6 @@ func TestParseBadDuration(t *testing.T) { cases := []string{ "1", "1y1m1d", - "-1w", "1.5d", "d", "294y", @@ -322,7 +422,6 @@ func TestParseBadDuration(t *testing.T) { if err == nil { t.Errorf("Expected error on input %s", c) } - } }