Skip to content

Commit

Permalink
Add support for more user-friendly duration input (#246)
Browse files Browse the repository at this point in the history
* Add support for more user-friendly duration input

With this pull request, it is possible to write:

6d23h

instead of

167h

Which is a lot more easy to read.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
  • Loading branch information
roidelapluie authored Aug 3, 2020
1 parent 8b085a8 commit 9c5aa1e
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 48 deletions.
93 changes: 45 additions & 48 deletions model/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ func (d *Duration) Type() string {
return "duration"
}

var durationRE = regexp.MustCompile("^([0-9]+)(y|w|d|h|m|s|ms)$")
var durationRE = regexp.MustCompile("^(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?$")

// 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.
Expand All @@ -191,67 +191,64 @@ func ParseDuration(durationStr string) (Duration, error) {
return 0, nil
}
matches := durationRE.FindStringSubmatch(durationStr)
if len(matches) != 3 {
if matches == nil {
return 0, fmt.Errorf("not a valid duration string: %q", durationStr)
}
var (
n, _ = strconv.Atoi(matches[1])
dur = time.Duration(n) * time.Millisecond
)
switch unit := matches[2]; unit {
case "y":
dur *= 1000 * 60 * 60 * 24 * 365
case "w":
dur *= 1000 * 60 * 60 * 24 * 7
case "d":
dur *= 1000 * 60 * 60 * 24
case "h":
dur *= 1000 * 60 * 60
case "m":
dur *= 1000 * 60
case "s":
dur *= 1000
case "ms":
// Value already correct
default:
return 0, fmt.Errorf("invalid time unit in duration string: %q", unit)
var dur time.Duration

// Parse the match at pos `pos` in the regex and use `mult` to turn that
// into ms, then add that value to the total parsed duration.
m := func(pos int, mult time.Duration) {
if matches[pos] == "" {
return
}
n, _ := strconv.Atoi(matches[pos])
d := time.Duration(n) * time.Millisecond
dur += d * mult
}

m(2, 1000*60*60*24*365) // y
m(4, 1000*60*60*24*7) // w
m(6, 1000*60*60*24) // d
m(8, 1000*60*60) // h
m(10, 1000*60) // m
m(12, 1000) // s
m(14, 1) // ms

return Duration(dur), nil
}

func (d Duration) String() string {
var (
ms = int64(time.Duration(d) / time.Millisecond)
unit = "ms"
ms = int64(time.Duration(d) / time.Millisecond)
r = ""
)
if ms == 0 {
return "0s"
}
factors := map[string]int64{
"y": 1000 * 60 * 60 * 24 * 365,
"w": 1000 * 60 * 60 * 24 * 7,
"d": 1000 * 60 * 60 * 24,
"h": 1000 * 60 * 60,
"m": 1000 * 60,
"s": 1000,
"ms": 1,
}

switch int64(0) {
case ms % factors["y"]:
unit = "y"
case ms % factors["w"]:
unit = "w"
case ms % factors["d"]:
unit = "d"
case ms % factors["h"]:
unit = "h"
case ms % factors["m"]:
unit = "m"
case ms % factors["s"]:
unit = "s"
f := func(unit string, mult int64, exact bool) {
if exact && ms%mult != 0 {
return
}
if v := ms / mult; v > 0 {
r += fmt.Sprintf("%d%s", v, unit)
ms -= v * mult
}
}
return fmt.Sprintf("%v%v", ms/factors[unit], unit)

// Only format years and weeks if the remainder is zero, as it is often
// easier to read 90d than 12w6d.
f("y", 1000*60*60*24*365, true)
f("w", 1000*60*60*24*7, true)

f("d", 1000*60*60*24, false)
f("h", 1000*60*60, false)
f("m", 1000*60, false)
f("s", 1000, false)
f("ms", 1, false)

return r
}

// MarshalYAML implements the yaml.Marshaler interface.
Expand Down
32 changes: 32 additions & 0 deletions model/time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func TestParseDuration(t *testing.T) {
in: "0",
out: 0,
expectedString: "0s",
}, {
in: "0w",
out: 0,
expectedString: "0s",
}, {
in: "0s",
out: 0,
Expand All @@ -115,9 +119,20 @@ func TestParseDuration(t *testing.T) {
}, {
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,
Expand All @@ -142,6 +157,23 @@ func TestParseDuration(t *testing.T) {
}
}

func TestParseBadDuration(t *testing.T) {
var cases = []string{
"1",
"1y1m1d",
"-1w",
"1.5d",
}

for _, c := range cases {
_, err := ParseDuration(c)
if err == nil {
t.Errorf("Expected error on input %s", c)
}

}
}

func TestTimeJSON(t *testing.T) {
tests := []struct {
in Time
Expand Down

0 comments on commit 9c5aa1e

Please sign in to comment.