Skip to content

Commit 0bc4c5a

Browse files
committed
Using standard ParseDuration implementation
Added to standard implementation days and weeks.
1 parent 18ebd3a commit 0bc4c5a

File tree

2 files changed

+146
-122
lines changed

2 files changed

+146
-122
lines changed

str2duration.go

Lines changed: 146 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -2,151 +2,179 @@ package str2duration
22

33
import (
44
"errors"
5-
"regexp"
6-
"strconv"
7-
"strings"
85
"time"
96
)
107

11-
const (
12-
hoursByDay = 24 //hours in a day
13-
hoursByWeek = 168 //hours in a weekend
14-
)
15-
16-
/*
17-
DisableCheck speed up performance disabling aditional checks
18-
in the input string. If DisableCheck is true then when input string is
19-
is invalid the time.Duration returned is always 0s and err is always nil.
20-
By default DisableCheck is false.
21-
*/
22-
var DisableCheck bool
23-
var reTimeDecimal *regexp.Regexp
24-
var reDuration *regexp.Regexp
25-
26-
func init() {
27-
reTimeDecimal = regexp.MustCompile(`(?i)(\d+)(?:(?:\.)(\d+))?((?:[mµn])?s)$`)
28-
reDuration = regexp.MustCompile(`(?i)^(?:(\d+)(?:w))?(?:(\d+)(?:d))?(?:(\d+)(?:h))?(?:(\d{1,2})(?:m))?(?:(\d+)(?:s))?(?:(\d+)(?:ms))?(?:(\d+)(?:(?:µ|u)s))?(?:(\d+)(?:ns))?$`)
8+
var unitMap = map[string]int64{
9+
"ns": int64(time.Nanosecond),
10+
"us": int64(time.Microsecond),
11+
"µs": int64(time.Microsecond), // U+00B5 = micro symbol
12+
"μs": int64(time.Microsecond), // U+03BC = Greek letter mu
13+
"ms": int64(time.Millisecond),
14+
"s": int64(time.Second),
15+
"m": int64(time.Minute),
16+
"h": int64(time.Hour),
17+
"d": int64(time.Hour) * 24,
18+
"w": int64(time.Hour) * 168,
2919
}
3020

31-
//Str2Duration returns time.Duration from string input
32-
func Str2Duration(str string) (time.Duration, error) {
33-
34-
var err error
35-
/*
36-
Go time.Duration string can returns lower times like nano, micro and milli seconds in decimal
37-
format, for example, 1 second with 1 nano second is 1.000000001s. For this when a dot is in the
38-
string then that time is formatted in nanoseconds, this example returns 1000000001ns
39-
*/
40-
if strings.Contains(str, ".") {
41-
str, err = decimalTimeToNano(str)
42-
if err != nil {
43-
return time.Duration(0), err
21+
// Str2Duration parses a duration string.
22+
// A duration string is a possibly signed sequence of
23+
// decimal numbers, each with optional fraction and a unit suffix,
24+
// such as "300ms", "-1.5h" or "2h45m".
25+
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "w".
26+
func Str2Duration(s string) (time.Duration, error) {
27+
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
28+
orig := s
29+
var d int64
30+
neg := false
31+
32+
// Consume [-+]?
33+
if s != "" {
34+
c := s[0]
35+
if c == '-' || c == '+' {
36+
neg = c == '-'
37+
s = s[1:]
4438
}
4539
}
46-
47-
if !DisableCheck {
48-
if !reDuration.MatchString(str) {
49-
return time.Duration(0), errors.New("invalid input duration string")
50-
}
40+
// Special case: if all that is left is "0", this is zero.
41+
if s == "0" {
42+
return 0, nil
5143
}
44+
if s == "" {
45+
return 0, errors.New("time: invalid duration " + quote(orig))
46+
}
47+
for s != "" {
48+
var (
49+
v, f int64 // integers before, after decimal point
50+
scale float64 = 1 // value = v + f/scale
51+
)
5252

53-
var du time.Duration
54-
55-
//errors ignored because regex
56-
for _, match := range reDuration.FindAllStringSubmatch(str, -1) {
53+
var err error
5754

58-
//weeks
59-
if len(match[1]) > 0 {
60-
w, _ := strconv.Atoi(match[1])
61-
du += time.Duration(w*hoursByWeek) * time.Hour
55+
// The next character must be [0-9.]
56+
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') {
57+
return 0, errors.New("time: invalid duration " + quote(orig))
6258
}
63-
64-
//days
65-
if len(match[2]) > 0 {
66-
d, _ := strconv.Atoi(match[2])
67-
du += time.Duration(d*hoursByDay) * time.Hour
59+
// Consume [0-9]*
60+
pl := len(s)
61+
v, s, err = leadingInt(s)
62+
if err != nil {
63+
return 0, errors.New("time: invalid duration " + quote(orig))
6864
}
69-
70-
//hours
71-
if len(match[3]) > 0 {
72-
h, _ := strconv.Atoi(match[3])
73-
du += time.Duration(h) * time.Hour
65+
pre := pl != len(s) // whether we consumed anything before a period
66+
67+
// Consume (\.[0-9]*)?
68+
post := false
69+
if s != "" && s[0] == '.' {
70+
s = s[1:]
71+
pl := len(s)
72+
f, scale, s = leadingFraction(s)
73+
post = pl != len(s)
7474
}
75-
76-
//minutes
77-
if len(match[4]) > 0 {
78-
m, _ := strconv.Atoi(match[4])
79-
du += time.Duration(m) * time.Minute
75+
if !pre && !post {
76+
// no digits (e.g. ".s" or "-.s")
77+
return 0, errors.New("time: invalid duration " + quote(orig))
8078
}
8179

82-
//seconds
83-
if len(match[5]) > 0 {
84-
s, _ := strconv.Atoi(match[5])
85-
du += time.Duration(s) * time.Second
80+
// Consume unit.
81+
i := 0
82+
for ; i < len(s); i++ {
83+
c := s[i]
84+
if c == '.' || '0' <= c && c <= '9' {
85+
break
86+
}
8687
}
87-
88-
//milliseconds
89-
if len(match[6]) > 0 {
90-
ms, _ := strconv.Atoi(match[6])
91-
du += time.Duration(ms) * time.Millisecond
88+
if i == 0 {
89+
return 0, errors.New("time: missing unit in duration " + quote(orig))
9290
}
93-
94-
//microseconds
95-
if len(match[7]) > 0 {
96-
ms, _ := strconv.Atoi(match[7])
97-
du += time.Duration(ms) * time.Microsecond
91+
u := s[:i]
92+
s = s[i:]
93+
unit, ok := unitMap[u]
94+
if !ok {
95+
return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig))
9896
}
99-
100-
//nanoseconds
101-
if len(match[8]) > 0 {
102-
ns, _ := strconv.Atoi(match[8])
103-
du += time.Duration(ns) * time.Nanosecond
97+
if v > (1<<63-1)/unit {
98+
// overflow
99+
return 0, errors.New("time: invalid duration " + quote(orig))
100+
}
101+
v *= unit
102+
if f > 0 {
103+
// float64 is needed to be nanosecond accurate for fractions of hours.
104+
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
105+
v += int64(float64(f) * (float64(unit) / scale))
106+
if v < 0 {
107+
// overflow
108+
return 0, errors.New("time: invalid duration " + quote(orig))
109+
}
110+
}
111+
d += v
112+
if d < 0 {
113+
// overflow
114+
return 0, errors.New("time: invalid duration " + quote(orig))
104115
}
105116
}
106117

107-
return du, nil
118+
if neg {
119+
d = -d
120+
}
121+
return time.Duration(d), nil
108122
}
109123

110-
func decimalTimeToNano(str string) (string, error) {
124+
func quote(s string) string {
125+
return "\"" + s + "\""
126+
}
111127

112-
var dotPart, dotTime, dotTimeDecimal, dotUnit string
128+
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
113129

114-
if !DisableCheck {
115-
if !reTimeDecimal.MatchString(str) {
116-
return "", errors.New("invalid input duration string")
130+
// leadingInt consumes the leading [0-9]* from s.
131+
func leadingInt(s string) (x int64, rem string, err error) {
132+
i := 0
133+
for ; i < len(s); i++ {
134+
c := s[i]
135+
if c < '0' || c > '9' {
136+
break
137+
}
138+
if x > (1<<63-1)/10 {
139+
// overflow
140+
return 0, "", errLeadingInt
141+
}
142+
x = x*10 + int64(c) - '0'
143+
if x < 0 {
144+
// overflow
145+
return 0, "", errLeadingInt
117146
}
118147
}
148+
return x, s[i:], nil
149+
}
119150

120-
var t = reTimeDecimal.FindAllStringSubmatch(str, -1)
121-
122-
dotPart = t[0][0]
123-
dotTime = t[0][1]
124-
dotTimeDecimal = t[0][2]
125-
dotUnit = t[0][3]
126-
127-
nanoSeconds := 1
128-
switch dotUnit {
129-
case "s":
130-
nanoSeconds = 1000000000
131-
dotTimeDecimal += strings.Repeat("0", 9-len(dotTimeDecimal))
132-
case "ms":
133-
nanoSeconds = 1000000
134-
dotTimeDecimal += strings.Repeat("0", 6-len(dotTimeDecimal))
135-
case "µs", "us":
136-
nanoSeconds = 1000
137-
dotTimeDecimal += strings.Repeat("0", 3-len(dotTimeDecimal))
151+
// leadingFraction consumes the leading [0-9]* from s.
152+
// It is used only for fractions, so does not return an error on overflow,
153+
// it just stops accumulating precision.
154+
func leadingFraction(s string) (x int64, scale float64, rem string) {
155+
i := 0
156+
scale = 1
157+
overflow := false
158+
for ; i < len(s); i++ {
159+
c := s[i]
160+
if c < '0' || c > '9' {
161+
break
162+
}
163+
if overflow {
164+
continue
165+
}
166+
if x > (1<<63-1)/10 {
167+
// It's possible for overflow to give a positive number, so take care.
168+
overflow = true
169+
continue
170+
}
171+
y := x*10 + int64(c) - '0'
172+
if y < 0 {
173+
overflow = true
174+
continue
175+
}
176+
x = y
177+
scale *= 10
138178
}
139-
140-
//errors ignored because regex
141-
142-
//timeMajor is the part decimal before point
143-
timeMajor, _ := strconv.Atoi(dotTime)
144-
timeMajor = timeMajor * nanoSeconds
145-
146-
//timeMajor is the part in decimal after point
147-
timeMinor, _ := strconv.Atoi(dotTimeDecimal)
148-
149-
newNanoTime := timeMajor + timeMinor
150-
151-
return strings.Replace(str, dotPart, strconv.Itoa(newNanoTime)+"ns", 1), nil
179+
return x, scale, s[i:]
152180
}

str2duration_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77

88
func TestParseString(t *testing.T) {
99

10-
DisableCheck = false
11-
1210
for i, tt := range []struct {
1311
dur string
1412
expected time.Duration
@@ -61,8 +59,6 @@ func TestParseString(t *testing.T) {
6159
//TestParseDuration test if string returned by a duration is equal to string returned with the package
6260
func TestParseDuration(t *testing.T) {
6361

64-
DisableCheck = true
65-
6662
for i, duration := range []time.Duration{
6763
time.Duration(time.Hour + time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
6864
time.Duration(time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),

0 commit comments

Comments
 (0)