Skip to content

Commit

Permalink
Using standard ParseDuration implementation
Browse files Browse the repository at this point in the history
Added to standard implementation days and weeks.
  • Loading branch information
xhit committed Aug 8, 2020
1 parent 18ebd3a commit 0bc4c5a
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 122 deletions.
264 changes: 146 additions & 118 deletions str2duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,151 +2,179 @@ package str2duration

import (
"errors"
"regexp"
"strconv"
"strings"
"time"
)

const (
hoursByDay = 24 //hours in a day
hoursByWeek = 168 //hours in a weekend
)

/*
DisableCheck speed up performance disabling aditional checks
in the input string. If DisableCheck is true then when input string is
is invalid the time.Duration returned is always 0s and err is always nil.
By default DisableCheck is false.
*/
var DisableCheck bool
var reTimeDecimal *regexp.Regexp
var reDuration *regexp.Regexp

func init() {
reTimeDecimal = regexp.MustCompile(`(?i)(\d+)(?:(?:\.)(\d+))?((?:[mµn])?s)$`)
reDuration = regexp.MustCompile(`(?i)^(?:(\d+)(?:w))?(?:(\d+)(?:d))?(?:(\d+)(?:h))?(?:(\d{1,2})(?:m))?(?:(\d+)(?:s))?(?:(\d+)(?:ms))?(?:(\d+)(?:(?:µ|u)s))?(?:(\d+)(?:ns))?$`)
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,
}

//Str2Duration returns time.Duration from string input
func Str2Duration(str string) (time.Duration, error) {

var err error
/*
Go time.Duration string can returns lower times like nano, micro and milli seconds in decimal
format, for example, 1 second with 1 nano second is 1.000000001s. For this when a dot is in the
string then that time is formatted in nanoseconds, this example returns 1000000001ns
*/
if strings.Contains(str, ".") {
str, err = decimalTimeToNano(str)
if err != nil {
return time.Duration(0), err
// Str2Duration 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 Str2Duration(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:]
}
}

if !DisableCheck {
if !reDuration.MatchString(str) {
return time.Duration(0), errors.New("invalid input duration string")
}
// 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 du time.Duration

//errors ignored because regex
for _, match := range reDuration.FindAllStringSubmatch(str, -1) {
var err error

//weeks
if len(match[1]) > 0 {
w, _ := strconv.Atoi(match[1])
du += time.Duration(w*hoursByWeek) * time.Hour
// 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))
}

//days
if len(match[2]) > 0 {
d, _ := strconv.Atoi(match[2])
du += time.Duration(d*hoursByDay) * time.Hour
// Consume [0-9]*
pl := len(s)
v, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + quote(orig))
}

//hours
if len(match[3]) > 0 {
h, _ := strconv.Atoi(match[3])
du += time.Duration(h) * time.Hour
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)
}

//minutes
if len(match[4]) > 0 {
m, _ := strconv.Atoi(match[4])
du += time.Duration(m) * time.Minute
if !pre && !post {
// no digits (e.g. ".s" or "-.s")
return 0, errors.New("time: invalid duration " + quote(orig))
}

//seconds
if len(match[5]) > 0 {
s, _ := strconv.Atoi(match[5])
du += time.Duration(s) * time.Second
// Consume unit.
i := 0
for ; i < len(s); i++ {
c := s[i]
if c == '.' || '0' <= c && c <= '9' {
break
}
}

//milliseconds
if len(match[6]) > 0 {
ms, _ := strconv.Atoi(match[6])
du += time.Duration(ms) * time.Millisecond
if i == 0 {
return 0, errors.New("time: missing unit in duration " + quote(orig))
}

//microseconds
if len(match[7]) > 0 {
ms, _ := strconv.Atoi(match[7])
du += time.Duration(ms) * time.Microsecond
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))
}

//nanoseconds
if len(match[8]) > 0 {
ns, _ := strconv.Atoi(match[8])
du += time.Duration(ns) * time.Nanosecond
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))
}
}

return du, nil
if neg {
d = -d
}
return time.Duration(d), nil
}

func decimalTimeToNano(str string) (string, error) {
func quote(s string) string {
return "\"" + s + "\""
}

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

if !DisableCheck {
if !reTimeDecimal.MatchString(str) {
return "", errors.New("invalid input duration string")
// 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
}

var t = reTimeDecimal.FindAllStringSubmatch(str, -1)

dotPart = t[0][0]
dotTime = t[0][1]
dotTimeDecimal = t[0][2]
dotUnit = t[0][3]

nanoSeconds := 1
switch dotUnit {
case "s":
nanoSeconds = 1000000000
dotTimeDecimal += strings.Repeat("0", 9-len(dotTimeDecimal))
case "ms":
nanoSeconds = 1000000
dotTimeDecimal += strings.Repeat("0", 6-len(dotTimeDecimal))
case "µs", "us":
nanoSeconds = 1000
dotTimeDecimal += strings.Repeat("0", 3-len(dotTimeDecimal))
// 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
}

//errors ignored because regex

//timeMajor is the part decimal before point
timeMajor, _ := strconv.Atoi(dotTime)
timeMajor = timeMajor * nanoSeconds

//timeMajor is the part in decimal after point
timeMinor, _ := strconv.Atoi(dotTimeDecimal)

newNanoTime := timeMajor + timeMinor

return strings.Replace(str, dotPart, strconv.Itoa(newNanoTime)+"ns", 1), nil
return x, scale, s[i:]
}
4 changes: 0 additions & 4 deletions str2duration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (

func TestParseString(t *testing.T) {

DisableCheck = false

for i, tt := range []struct {
dur string
expected time.Duration
Expand Down Expand Up @@ -61,8 +59,6 @@ func TestParseString(t *testing.T) {
//TestParseDuration test if string returned by a duration is equal to string returned with the package
func TestParseDuration(t *testing.T) {

DisableCheck = true

for i, duration := range []time.Duration{
time.Duration(time.Hour + time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
time.Duration(time.Minute + time.Second + time.Millisecond + time.Microsecond + time.Nanosecond),
Expand Down

0 comments on commit 0bc4c5a

Please sign in to comment.