Skip to content

Commit d7d98af

Browse files
author
Julien Pivotto
authored
Merge pull request #461 from bboreham/faster-parseduration
Parse Durations much faster
2 parents 94c865c + 55b01d1 commit d7d98af

File tree

2 files changed

+67
-33
lines changed

2 files changed

+67
-33
lines changed

model/time.go

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"errors"
1919
"fmt"
2020
"math"
21-
"regexp"
2221
"strconv"
2322
"strings"
2423
"time"
@@ -183,54 +182,78 @@ func (d *Duration) Type() string {
183182
return "duration"
184183
}
185184

186-
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)?$")
185+
func isdigit(c byte) bool { return c >= '0' && c <= '9' }
186+
187+
// Units are required to go in order from biggest to smallest.
188+
// This guards against confusion from "1m1d" being 1 minute + 1 day, not 1 month + 1 day.
189+
var unitMap = map[string]struct {
190+
pos int
191+
mult uint64
192+
}{
193+
"ms": {7, uint64(time.Millisecond)},
194+
"s": {6, uint64(time.Second)},
195+
"m": {5, uint64(time.Minute)},
196+
"h": {4, uint64(time.Hour)},
197+
"d": {3, uint64(24 * time.Hour)},
198+
"w": {2, uint64(7 * 24 * time.Hour)},
199+
"y": {1, uint64(365 * 24 * time.Hour)},
200+
}
187201

188202
// ParseDuration parses a string into a time.Duration, assuming that a year
189203
// always has 365d, a week always has 7d, and a day always has 24h.
190-
func ParseDuration(durationStr string) (Duration, error) {
191-
switch durationStr {
204+
func ParseDuration(s string) (Duration, error) {
205+
switch s {
192206
case "0":
193207
// Allow 0 without a unit.
194208
return 0, nil
195209
case "":
196210
return 0, errors.New("empty duration string")
197211
}
198-
matches := durationRE.FindStringSubmatch(durationStr)
199-
if matches == nil {
200-
return 0, fmt.Errorf("not a valid duration string: %q", durationStr)
201-
}
202-
var dur time.Duration
203212

204-
// Parse the match at pos `pos` in the regex and use `mult` to turn that
205-
// into ms, then add that value to the total parsed duration.
206-
var overflowErr error
207-
m := func(pos int, mult time.Duration) {
208-
if matches[pos] == "" {
209-
return
213+
orig := s
214+
var dur uint64
215+
lastUnitPos := 0
216+
217+
for s != "" {
218+
if !isdigit(s[0]) {
219+
return 0, fmt.Errorf("not a valid duration string: %q", orig)
220+
}
221+
// Consume [0-9]*
222+
i := 0
223+
for ; i < len(s) && isdigit(s[i]); i++ {
224+
}
225+
v, err := strconv.ParseUint(s[:i], 10, 0)
226+
if err != nil {
227+
return 0, fmt.Errorf("not a valid duration string: %q", orig)
210228
}
211-
n, _ := strconv.Atoi(matches[pos])
229+
s = s[i:]
212230

231+
// Consume unit.
232+
for i = 0; i < len(s) && !isdigit(s[i]); i++ {
233+
}
234+
if i == 0 {
235+
return 0, fmt.Errorf("not a valid duration string: %q", orig)
236+
}
237+
u := s[:i]
238+
s = s[i:]
239+
unit, ok := unitMap[u]
240+
if !ok {
241+
return 0, fmt.Errorf("unknown unit %q in duration %q", u, orig)
242+
}
243+
if unit.pos <= lastUnitPos { // Units must go in order from biggest to smallest.
244+
return 0, fmt.Errorf("not a valid duration string: %q", orig)
245+
}
246+
lastUnitPos = unit.pos
213247
// Check if the provided duration overflows time.Duration (> ~ 290years).
214-
if n > int((1<<63-1)/mult/time.Millisecond) {
215-
overflowErr = errors.New("duration out of range")
248+
if v > 1<<63/unit.mult {
249+
return 0, errors.New("duration out of range")
216250
}
217-
d := time.Duration(n) * time.Millisecond
218-
dur += d * mult
219-
220-
if dur < 0 {
221-
overflowErr = errors.New("duration out of range")
251+
dur += v * unit.mult
252+
if dur > 1<<63-1 {
253+
return 0, errors.New("duration out of range")
222254
}
223255
}
224-
225-
m(2, 1000*60*60*24*365) // y
226-
m(4, 1000*60*60*24*7) // w
227-
m(6, 1000*60*60*24) // d
228-
m(8, 1000*60*60) // h
229-
m(10, 1000*60) // m
230-
m(12, 1000) // s
231-
m(14, 1) // ms
232-
233-
return Duration(dur), overflowErr
256+
return Duration(dur), nil
234257
}
235258

236259
func (d Duration) String() string {

model/time_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,14 @@ func TestTimeJSON(t *testing.T) {
367367
}
368368

369369
}
370+
371+
func BenchmarkParseDuration(b *testing.B) {
372+
const data = "30s"
373+
374+
for i := 0; i < b.N; i++ {
375+
_, err := ParseDuration(data)
376+
if err != nil {
377+
b.Fatal(err)
378+
}
379+
}
380+
}

0 commit comments

Comments
 (0)