Skip to content

Commit ccda8ef

Browse files
committed
release 1.0
0 parents  commit ccda8ef

File tree

5 files changed

+386
-0
lines changed

5 files changed

+386
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2019 Santiago De la Cruz
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Go String To Duration (go-str2duration)
2+
3+
This package allows to get a time.Duration from a string. The string can be a string retorned for time.Duration or a similar string with weeks or days too!.
4+
5+
[![Go Doc](https://godoc.org/github.com/xhit/go-str2duration?status.svg)](https://godoc.org/github.com/xhit/go-str2duration)
6+
[![Go Report](https://goreportcard.com/badge/github.com/xhit/go-str2duration)](https://goreportcard.com/report/github.com/xhit/go-str2duration)
7+
8+
## Download
9+
10+
```bash
11+
go get github.com/xhit/go-str2duration
12+
```
13+
14+
## Features
15+
16+
Go String To Duration supports this strings conversions to duration:
17+
- All strings returned in time.Duration String.
18+
- A string more readable like 1w2d6h3ns (1 week 2 days 6 hours and 3 nanoseconds)
19+
20+
## Use cases
21+
22+
- Imagine you save the output of time.Duration strings in a database, file, etc... and you need to convert again to time.Duration. Now you can!
23+
- Set a more precise duration in a configuration file for wait, timeouts, measure, etc...
24+
25+
## Usage
26+
27+
```go
28+
package main
29+
30+
import (
31+
"fmt"
32+
str2duration "github.com/xhit/go-str2duration"
33+
"os"
34+
"time"
35+
)
36+
37+
func main() {
38+
39+
//Initialize the parser
40+
s2dParser := str2duration.NewStr2DurationParser()
41+
42+
/*
43+
If DisableCheck is true then when input string is
44+
is invalid the time.Duration returned is always 0s and err is always nil.
45+
By default DisableCheck is false.
46+
*/
47+
48+
//s2dParser.DisableCheck = true
49+
50+
for i, tt := range []struct {
51+
dur string
52+
expected time.Duration
53+
}{
54+
//This times are returned with time.Duration string
55+
{"1h", time.Duration(time.Hour)},
56+
{"1m", time.Duration(time.Minute)},
57+
{"1s", time.Duration(time.Second)},
58+
{"1ms", time.Duration(time.Millisecond)},
59+
{"1µs", time.Duration(time.Microsecond)},
60+
{"1ns", time.Duration(time.Nanosecond)},
61+
{"4.000000001s", time.Duration(4*time.Second + time.Nanosecond)},
62+
{"1h0m4.000000001s", time.Duration(time.Hour + 4*time.Second + time.Nanosecond)},
63+
{"1h1m0.01s", time.Duration(61*time.Minute + 10*time.Millisecond)},
64+
{"1h1m0.123456789s", time.Duration(61*time.Minute + 123456789*time.Nanosecond)},
65+
{"1.00002ms", time.Duration(time.Millisecond + 20*time.Nanosecond)},
66+
{"1.00000002s", time.Duration(time.Second + 20*time.Nanosecond)},
67+
{"693ns", time.Duration(693 * time.Nanosecond)},
68+
69+
//This times aren't returned with time.Duration string, but are easily readable and can be parsed too!
70+
{"1ms1ns", time.Duration(time.Millisecond + 1*time.Nanosecond)},
71+
{"1s20ns", time.Duration(time.Second + 20*time.Nanosecond)},
72+
{"60h8ms", time.Duration(60*time.Hour + 8*time.Millisecond)},
73+
{"96h63s", time.Duration(96*time.Hour + 63*time.Second)},
74+
75+
//And works with days and weeks!
76+
{"2d3s96ns", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
77+
{"1w2d3s96ns", time.Duration(168*time.Hour + 48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
78+
79+
//And can be case insensitive
80+
{"2D3S96NS", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
81+
82+
} {
83+
durationFromString, err := s2dParser.Str2Duration(tt.dur)
84+
if err != nil {
85+
panic(err)
86+
87+
//Check if expected time is the time returned by the parser
88+
} else if tt.expected != durationFromString {
89+
fmt.Println(fmt.Sprintf("index %d -> in: %s returned: %s\tnot equal to %s", i, tt.dur, durationFromString.String(), tt.expected.String()))
90+
}else{
91+
fmt.Println(fmt.Sprintf("index %d -> in: %s parsed succesfully", i, tt.dur))
92+
}
93+
}
94+
}
95+
```

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/xhit/go-str2duration
2+
3+
go 1.13

str2duration.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package str2duration
2+
3+
import (
4+
"errors"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
"time"
9+
)
10+
11+
const (
12+
hoursByDay = 24 //hours in a day
13+
hoursByWeek = 168 //hours in a weekend
14+
)
15+
16+
/*
17+
Parser struct contains flag
18+
for DisableCheck the speed up performance disabling aditional checks
19+
in the input string. If DisableCheck is true then when input string is
20+
is invalid the time.Duration returned is always 0s and err is always nil.
21+
By default DisableCheck is false.
22+
23+
Parser also contains compiled regex for performance.
24+
*/
25+
type Parser struct {
26+
reTimeDecimal *regexp.Regexp
27+
reDuration *regexp.Regexp
28+
DisableCheck bool
29+
}
30+
31+
//NewStr2DurationParser returns unexported struct with compiled regex for performance
32+
func NewStr2DurationParser() Parser {
33+
var reTimeDecimal = regexp.MustCompile(`(?i)(\d+)(?:(?:\.)(\d+))?((?:[mµn])?s)$`)
34+
var reDuration = regexp.MustCompile(`(?i)^(?:(\d+)(?:w))?(?:(\d+)(?:d))?(?:(\d+)(?:h))?(?:(\d{1,2})(?:m))?(?:(\d+)(?:s))?(?:(\d+)(?:ms))?(?:(\d+)(?:µs))?(?:(\d+)(?:ns))?$`)
35+
36+
return Parser{
37+
reTimeDecimal: reTimeDecimal,
38+
reDuration: reDuration,
39+
}
40+
}
41+
42+
//Str2Duration returns time.Duration from string input
43+
func (p *Parser) Str2Duration(str string) (time.Duration, error) {
44+
45+
var err error
46+
/*
47+
Go time.Duration string can returns lower times like nano, micro and milli seconds in decimal
48+
format, for example, 1 second with 1 nano second is 1.000000001s. For this when a dot is in the
49+
string then that time is formatted in nanoseconds, this example returns 1000000001ns
50+
*/
51+
if strings.Contains(str, ".") {
52+
str, err = p.decimalTimeToNano(str)
53+
if err != nil {
54+
return time.Duration(0), err
55+
}
56+
}
57+
58+
if !p.DisableCheck {
59+
if !p.reDuration.MatchString(str) {
60+
return time.Duration(0), errors.New("invalid input duration string")
61+
}
62+
}
63+
64+
var du time.Duration
65+
66+
//errors ignored because regex
67+
for _, match := range p.reDuration.FindAllStringSubmatch(str, -1) {
68+
69+
//weeks
70+
if len(match[1]) > 0 {
71+
w, _ := strconv.Atoi(match[1])
72+
du += time.Duration(w*hoursByWeek) * time.Hour
73+
}
74+
75+
//days
76+
if len(match[2]) > 0 {
77+
d, _ := strconv.Atoi(match[2])
78+
du += time.Duration(d*hoursByDay) * time.Hour
79+
}
80+
81+
//hours
82+
if len(match[3]) > 0 {
83+
h, _ := strconv.Atoi(match[3])
84+
du += time.Duration(h) * time.Hour
85+
}
86+
87+
//minutes
88+
if len(match[4]) > 0 {
89+
m, _ := strconv.Atoi(match[4])
90+
du += time.Duration(m) * time.Minute
91+
}
92+
93+
//seconds
94+
if len(match[5]) > 0 {
95+
s, _ := strconv.Atoi(match[5])
96+
du += time.Duration(s) * time.Second
97+
}
98+
99+
//milliseconds
100+
if len(match[6]) > 0 {
101+
ms, _ := strconv.Atoi(match[6])
102+
du += time.Duration(ms) * time.Millisecond
103+
}
104+
105+
//microseconds
106+
if len(match[7]) > 0 {
107+
ms, _ := strconv.Atoi(match[7])
108+
du += time.Duration(ms) * time.Microsecond
109+
}
110+
111+
//nanoseconds
112+
if len(match[8]) > 0 {
113+
ns, _ := strconv.Atoi(match[8])
114+
du += time.Duration(ns) * time.Nanosecond
115+
}
116+
}
117+
118+
return du, nil
119+
}
120+
121+
func (p *Parser) decimalTimeToNano(str string) (string, error) {
122+
123+
var dotPart, dotTime, dotTimeDecimal, dotUnit string
124+
125+
if !p.DisableCheck {
126+
if !p.reTimeDecimal.MatchString(str) {
127+
return "", errors.New("invalid input duration string")
128+
}
129+
}
130+
131+
var t = p.reTimeDecimal.FindAllStringSubmatch(str, -1)
132+
133+
dotPart = t[0][0]
134+
dotTime = t[0][1]
135+
dotTimeDecimal = t[0][2]
136+
dotUnit = t[0][3]
137+
138+
nanoSeconds := 1
139+
switch dotUnit {
140+
case "s":
141+
nanoSeconds = 1000000000
142+
dotTimeDecimal += strings.Repeat("0", 9-len(dotTimeDecimal))
143+
case "ms":
144+
nanoSeconds = 1000000
145+
dotTimeDecimal += strings.Repeat("0", 6-len(dotTimeDecimal))
146+
case "µs":
147+
nanoSeconds = 1000
148+
dotTimeDecimal += strings.Repeat("0", 3-len(dotTimeDecimal))
149+
}
150+
151+
//errors ignored because regex
152+
153+
//timeMajor is the part decimal before point
154+
timeMajor, _ := strconv.Atoi(dotTime)
155+
timeMajor = timeMajor * nanoSeconds
156+
157+
//timeMajor is the part in decimal after point
158+
timeMinor, _ := strconv.Atoi(dotTimeDecimal)
159+
160+
newNanoTime := timeMajor + timeMinor
161+
162+
return strings.Replace(str, dotPart, strconv.Itoa(newNanoTime)+"ns", 1), nil
163+
}

str2duration_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package str2duration
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestParseString(t *testing.T) {
9+
10+
str2duration := NewStr2DurationParser()
11+
12+
str2duration.DisableCheck = false
13+
14+
for i, tt := range []struct {
15+
dur string
16+
expected time.Duration
17+
}{
18+
//This times are returned with time.Duration string
19+
{"1h", time.Duration(time.Hour)},
20+
{"1m", time.Duration(time.Minute)},
21+
{"1s", time.Duration(time.Second)},
22+
{"1ms", time.Duration(time.Millisecond)},
23+
{"1µs", time.Duration(time.Microsecond)},
24+
{"1ns", time.Duration(time.Nanosecond)},
25+
{"4.000000001s", time.Duration(4*time.Second + time.Nanosecond)},
26+
{"1h0m4.000000001s", time.Duration(time.Hour + 4*time.Second + time.Nanosecond)},
27+
{"1h1m0.01s", time.Duration(61*time.Minute + 10*time.Millisecond)},
28+
{"1h1m0.123456789s", time.Duration(61*time.Minute + 123456789*time.Nanosecond)},
29+
{"1.00002ms", time.Duration(time.Millisecond + 20*time.Nanosecond)},
30+
{"1.00000002s", time.Duration(time.Second + 20*time.Nanosecond)},
31+
{"693ns", time.Duration(693 * time.Nanosecond)},
32+
33+
//This times aren't returned with time.Duration string, but are easily readable and can be parsed too!
34+
{"1ms1ns", time.Duration(time.Millisecond + 1*time.Nanosecond)},
35+
{"1s20ns", time.Duration(time.Second + 20*time.Nanosecond)},
36+
{"60h8ms", time.Duration(60*time.Hour + 8*time.Millisecond)},
37+
{"96h63s", time.Duration(96*time.Hour + 63*time.Second)},
38+
39+
//And works with days and weeks!
40+
{"2d3s96ns", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
41+
{"1w2d3s96ns", time.Duration(168*time.Hour + 48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
42+
{"1w2d3s3µs96ns", time.Duration(168*time.Hour + 48*time.Hour + 3*time.Second + 3*time.Microsecond + 96*time.Nanosecond)},
43+
44+
//And can be case insensitive
45+
{"2D3S96NS", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
46+
47+
//This cases are invalid
48+
{"2.3D3S96NS", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
49+
{"2D3S3.66.SMS", time.Duration(48*time.Hour + 3*time.Second + 96*time.Nanosecond)},
50+
} {
51+
durationFromString, err := str2duration.Str2Duration(tt.dur)
52+
if err != nil {
53+
t.Logf("index %d -> in: %s returned: %s\tnot equal to %s", i, tt.dur, err.Error(), tt.expected.String())
54+
55+
} else if tt.expected != durationFromString {
56+
t.Errorf("index %d -> in: %s returned: %s\tnot equal to %s", i, tt.dur, durationFromString.String(), tt.expected.String())
57+
}
58+
}
59+
}
60+
61+
//TestParseDuration test if string returned by a duration is equal to string returned with the package
62+
func TestParseDuration(t *testing.T) {
63+
64+
str2duration := NewStr2DurationParser()
65+
66+
str2duration.DisableCheck = true
67+
68+
for i, duration := range []time.Duration{
69+
time.Duration(time.Hour + time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
70+
time.Duration(time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
71+
time.Duration(time.Hour + time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
72+
time.Duration(time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
73+
time.Duration(time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
74+
time.Duration(time.Millisecond + time.Microsecond + time.Nanosecond),
75+
time.Duration(time.Microsecond + time.Nanosecond),
76+
time.Duration(time.Nanosecond),
77+
time.Duration(time.Hour + time.Minute + time.Second + time.Millisecond + time.Microsecond),
78+
time.Duration(time.Minute + time.Second + time.Millisecond + time.Microsecond),
79+
time.Duration(time.Second + time.Millisecond + time.Microsecond),
80+
time.Duration(time.Millisecond + time.Microsecond),
81+
time.Duration(time.Microsecond),
82+
time.Duration(time.Hour + time.Minute + time.Second + time.Millisecond),
83+
time.Duration(time.Minute + time.Second + time.Millisecond),
84+
time.Duration(time.Second + time.Millisecond),
85+
time.Duration(time.Millisecond),
86+
time.Duration(time.Hour + time.Minute + time.Second),
87+
time.Duration(time.Minute + time.Second),
88+
time.Duration(time.Second),
89+
time.Duration(time.Hour + time.Minute),
90+
time.Duration(time.Minute),
91+
time.Duration(time.Hour),
92+
time.Duration(time.Millisecond + time.Nanosecond),
93+
time.Duration(1001001 * time.Microsecond),
94+
time.Duration(1001 * time.Nanosecond),
95+
time.Duration(61 * time.Minute),
96+
time.Duration(61 * time.Second),
97+
time.Duration(time.Microsecond + 16*time.Nanosecond),
98+
} {
99+
durationFromString, _ := str2duration.Str2Duration(duration.String())
100+
if duration.String() != durationFromString.String() {
101+
t.Errorf("index %d -> %s not equal to %s", i, duration.String(), durationFromString.String())
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)