Created
August 8, 2020 05:59
-
-
Save xhit/79c9e137e1cfe332076cdda9f5e24699 to your computer and use it in GitHub Desktop.
Golang ParseDuration with days and weeks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"errors" | |
"fmt" | |
"time" | |
) | |
func main() { | |
dur, _ := ParseDuration("1.000000001s") | |
fmt.Println(dur) | |
} | |
var unitMap = map[string]int64{ | |
"ns": int64(time.Nanosecond), | |
"us": int64(time.Microsecond), | |
"µs": int64(time.Microsecond), // U+00B5 = micro symbol | |
"μs": int64(time.Microsecond), // U+03BC = Greek letter mu | |
"ms": int64(time.Millisecond), | |
"s": int64(time.Second), | |
"m": int64(time.Minute), | |
"h": int64(time.Hour), | |
"d": int64(time.Hour) * 24, | |
"w": int64(time.Hour) * 168, | |
} | |
// ParseDuration parses a duration string. | |
// A duration string is a possibly signed sequence of | |
// decimal numbers, each with optional fraction and a unit suffix, | |
// such as "300ms", "-1.5h" or "2h45m". | |
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "w". | |
func ParseDuration(s string) (time.Duration, error) { | |
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ | |
orig := s | |
var d int64 | |
neg := false | |
// Consume [-+]? | |
if s != "" { | |
c := s[0] | |
if c == '-' || c == '+' { | |
neg = c == '-' | |
s = s[1:] | |
} | |
} | |
// Special case: if all that is left is "0", this is zero. | |
if s == "0" { | |
return 0, nil | |
} | |
if s == "" { | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
for s != "" { | |
var ( | |
v, f int64 // integers before, after decimal point | |
scale float64 = 1 // value = v + f/scale | |
) | |
var err error | |
// The next character must be [0-9.] | |
if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
// Consume [0-9]* | |
pl := len(s) | |
v, s, err = leadingInt(s) | |
if err != nil { | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
pre := pl != len(s) // whether we consumed anything before a period | |
// Consume (\.[0-9]*)? | |
post := false | |
if s != "" && s[0] == '.' { | |
s = s[1:] | |
pl := len(s) | |
f, scale, s = leadingFraction(s) | |
post = pl != len(s) | |
} | |
if !pre && !post { | |
// no digits (e.g. ".s" or "-.s") | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
// Consume unit. | |
i := 0 | |
for ; i < len(s); i++ { | |
c := s[i] | |
if c == '.' || '0' <= c && c <= '9' { | |
break | |
} | |
} | |
if i == 0 { | |
return 0, errors.New("time: missing unit in duration " + quote(orig)) | |
} | |
u := s[:i] | |
s = s[i:] | |
unit, ok := unitMap[u] | |
if !ok { | |
return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) | |
} | |
if v > (1<<63-1)/unit { | |
// overflow | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
v *= unit | |
if f > 0 { | |
// float64 is needed to be nanosecond accurate for fractions of hours. | |
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) | |
v += int64(float64(f) * (float64(unit) / scale)) | |
if v < 0 { | |
// overflow | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
} | |
d += v | |
if d < 0 { | |
// overflow | |
return 0, errors.New("time: invalid duration " + quote(orig)) | |
} | |
} | |
if neg { | |
d = -d | |
} | |
return time.Duration(d), nil | |
} | |
func quote(s string) string { | |
return "\"" + s + "\"" | |
} | |
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed | |
// leadingInt consumes the leading [0-9]* from s. | |
func leadingInt(s string) (x int64, rem string, err error) { | |
i := 0 | |
for ; i < len(s); i++ { | |
c := s[i] | |
if c < '0' || c > '9' { | |
break | |
} | |
if x > (1<<63-1)/10 { | |
// overflow | |
return 0, "", errLeadingInt | |
} | |
x = x*10 + int64(c) - '0' | |
if x < 0 { | |
// overflow | |
return 0, "", errLeadingInt | |
} | |
} | |
return x, s[i:], nil | |
} | |
// leadingFraction consumes the leading [0-9]* from s. | |
// It is used only for fractions, so does not return an error on overflow, | |
// it just stops accumulating precision. | |
func leadingFraction(s string) (x int64, scale float64, rem string) { | |
i := 0 | |
scale = 1 | |
overflow := false | |
for ; i < len(s); i++ { | |
c := s[i] | |
if c < '0' || c > '9' { | |
break | |
} | |
if overflow { | |
continue | |
} | |
if x > (1<<63-1)/10 { | |
// It's possible for overflow to give a positive number, so take care. | |
overflow = true | |
continue | |
} | |
y := x*10 + int64(c) - '0' | |
if y < 0 { | |
overflow = true | |
continue | |
} | |
x = y | |
scale *= 10 | |
} | |
return x, scale, s[i:] | |
} |
Has BSD-3-Clause license, original repo: https://github.com/xhit/go-str2duration
Wonderful, thank you!! I don't even have to turn it into a package and add tests now!
This is very good!
I've also simplified the code to make it easier to copy and use.
// ParseDuration parses a duration string.
// examples: "10d", "-1.5w" or "3Y4M5d".
// Add time units are "d"="D", "w"="W", "M", "y"="Y".
func ParseDuration(s string) (time.Duration, error) {
neg := false
if len(s) > 0 && s[0] == '-' {
neg = true
s = s[1:]
}
re := regexp.MustCompile(`(\d*\.\d+|\d+)[^\d]*`)
unitMap := map[string]time.Duration{
"d": 24,
"D": 24,
"w": 7 * 24,
"W": 7 * 24,
"M": 30 * 24,
"y": 365 * 24,
"Y": 365 * 24,
}
strs := re.FindAllString(s, -1)
var sumDur time.Duration
for _, str := range strs {
var _hours time.Duration = 1
for unit, hours := range unitMap {
if strings.Contains(str, unit) {
str = strings.ReplaceAll(str, unit, "h")
_hours = hours
break
}
}
dur, err := time.ParseDuration(str)
if err != nil {
return 0, err
}
sumDur += dur * _hours
}
if neg {
sumDur = -sumDur
}
return sumDur, nil
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This code doesn't have a license, so I can't use it. Would you mind adding a license? Hopefully MIT, so I can use this code in https://pkg.go.dev/go.bbkane.com/[email protected]/value#Duration :D