@@ -2,151 +2,179 @@ package str2duration
22
33import (
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}
0 commit comments