From 0bc4c5ad4f598c7a641f9f4de28e6f3a5e4cc8db Mon Sep 17 00:00:00 2001 From: Santiago De la Cruz Date: Sat, 8 Aug 2020 02:12:43 -0400 Subject: [PATCH] Using standard ParseDuration implementation Added to standard implementation days and weeks. --- str2duration.go | 264 ++++++++++++++++++++++++------------------- str2duration_test.go | 4 - 2 files changed, 146 insertions(+), 122 deletions(-) diff --git a/str2duration.go b/str2duration.go index 96cbcb1..4e23de6 100644 --- a/str2duration.go +++ b/str2duration.go @@ -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:] } diff --git a/str2duration_test.go b/str2duration_test.go index 27665d5..7380330 100644 --- a/str2duration_test.go +++ b/str2duration_test.go @@ -7,8 +7,6 @@ import ( func TestParseString(t *testing.T) { - DisableCheck = false - for i, tt := range []struct { dur string expected time.Duration @@ -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),