Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse Durations much faster #461

Merged
merged 2 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 56 additions & 33 deletions model/time.go
Expand Up @@ -18,7 +18,6 @@ import (
"errors"
"fmt"
"math"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -183,54 +182,78 @@ func (d *Duration) Type() string {
return "duration"
}

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)?$")
func isdigit(c byte) bool { return c >= '0' && c <= '9' }

// Units are required to go in order from biggest to smallest.
// This guards against confusion from "1m1d" being 1 minute + 1 day, not 1 month + 1 day.
var unitMap = map[string]struct {
pos int
mult uint64
}{
"ms": {7, uint64(time.Millisecond)},
"s": {6, uint64(time.Second)},
"m": {5, uint64(time.Minute)},
"h": {4, uint64(time.Hour)},
"d": {3, uint64(24 * time.Hour)},
"w": {2, uint64(7 * 24 * time.Hour)},
"y": {1, uint64(365 * 24 * time.Hour)},
}

// 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.
func ParseDuration(durationStr string) (Duration, error) {
switch durationStr {
func ParseDuration(s string) (Duration, error) {
switch s {
case "0":
// Allow 0 without a unit.
return 0, nil
case "":
return 0, errors.New("empty duration string")
}
matches := durationRE.FindStringSubmatch(durationStr)
if matches == nil {
return 0, fmt.Errorf("not a valid duration string: %q", durationStr)
}
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.
var overflowErr error
m := func(pos int, mult time.Duration) {
if matches[pos] == "" {
return
orig := s
var dur uint64
lastUnitPos := 0

for s != "" {
if !isdigit(s[0]) {
return 0, fmt.Errorf("not a valid duration string: %q", orig)
}
// Consume [0-9]*
i := 0
for ; i < len(s) && isdigit(s[i]); i++ {
}
v, err := strconv.ParseUint(s[:i], 10, 0)
if err != nil {
return 0, fmt.Errorf("not a valid duration string: %q", orig)
}
n, _ := strconv.Atoi(matches[pos])
s = s[i:]

// Consume unit.
for i = 0; i < len(s) && !isdigit(s[i]); i++ {
}
if i == 0 {
return 0, fmt.Errorf("not a valid duration string: %q", orig)
}
u := s[:i]
s = s[i:]
unit, ok := unitMap[u]
if !ok {
return 0, fmt.Errorf("unknown unit %q in duration %q", u, orig)
}
if unit.pos <= lastUnitPos { // Units must go in order from biggest to smallest.
return 0, fmt.Errorf("not a valid duration string: %q", orig)
}
lastUnitPos = unit.pos
// Check if the provided duration overflows time.Duration (> ~ 290years).
if n > int((1<<63-1)/mult/time.Millisecond) {
overflowErr = errors.New("duration out of range")
if v > 1<<63/unit.mult {
return 0, errors.New("duration out of range")
}
d := time.Duration(n) * time.Millisecond
dur += d * mult

if dur < 0 {
overflowErr = errors.New("duration out of range")
dur += v * unit.mult
if dur > 1<<63-1 {
return 0, errors.New("duration out of range")
}
}

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), overflowErr
return Duration(dur), nil
}

func (d Duration) String() string {
Expand Down
11 changes: 11 additions & 0 deletions model/time_test.go
Expand Up @@ -367,3 +367,14 @@ func TestTimeJSON(t *testing.T) {
}

}

func BenchmarkParseDuration(b *testing.B) {
const data = "30s"

for i := 0; i < b.N; i++ {
_, err := ParseDuration(data)
if err != nil {
b.Fatal(err)
}
}
}