├── .gitignore ├── README.md ├── duration.go ├── duration_test.go └── go.mod /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # duration 2 | 3 | Copy of stdlib's `time.Duration`, but `ParseDuration` accepts other units as well: 4 | 5 | - `d`: days (`24 * time.Hour`) 6 | - `w`: weeks (`7 * Day`) 7 | - `mo`: months (`30 * Day`) 8 | - `y`: years (`365 * Day`) 9 | 10 | This is specially useful if you want to accept duration as flags in your program, and those duration are expected to be high. 11 | Asking the user to type `7860h` instead of `1y` might be something you don't want to. 12 | If so, you can accept the flag as string and parse with this package instead. 13 | 14 | Hopefully, someday, something like this gets merged into the stdlib. 15 | -------------------------------------------------------------------------------- /duration.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the [Golang repository](https://github.com/golang/go/blob/master/LICENSE). 4 | 5 | package duration 6 | 7 | import ( 8 | "errors" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | const ( 14 | Day time.Duration = 24 * time.Hour 15 | Week = 7 * Day 16 | Month = 30 * Day 17 | Year = 365 * Day 18 | ) 19 | 20 | func quote(s string) string { 21 | return "\"" + s + "\"" 22 | } 23 | 24 | var errLeadingInt = errors.New("time: bad [0-9]*") // never printed 25 | 26 | // leadingInt consumes the leading [0-9]* from s. 27 | func leadingInt(s string) (x int64, rem string, err error) { 28 | i := 0 29 | for ; i < len(s); i++ { 30 | c := s[i] 31 | if c < '0' || c > '9' { 32 | break 33 | } 34 | if x > (1<<63-1)/10 { 35 | // overflow 36 | return 0, "", errLeadingInt 37 | } 38 | x = x*10 + int64(c) - '0' 39 | if x < 0 { 40 | // overflow 41 | return 0, "", errLeadingInt 42 | } 43 | } 44 | return x, s[i:], nil 45 | } 46 | 47 | // leadingFraction consumes the leading [0-9]* from s. 48 | // It is used only for fractions, so does not return an error on overflow, 49 | // it just stops accumulating precision. 50 | func leadingFraction(s string) (x int64, scale float64, rem string) { 51 | i := 0 52 | scale = 1 53 | overflow := false 54 | for ; i < len(s); i++ { 55 | c := s[i] 56 | if c < '0' || c > '9' { 57 | break 58 | } 59 | if overflow { 60 | continue 61 | } 62 | if x > (1<<63-1)/10 { 63 | // It's possible for overflow to give a positive number, so take care. 64 | overflow = true 65 | continue 66 | } 67 | y := x*10 + int64(c) - '0' 68 | if y < 0 { 69 | overflow = true 70 | continue 71 | } 72 | x = y 73 | scale *= 10 74 | } 75 | return x, scale, s[i:] 76 | } 77 | 78 | var unitMap = map[string]int64{ 79 | "ns": int64(time.Nanosecond), 80 | "us": int64(time.Microsecond), 81 | "µs": int64(time.Microsecond), // U+00B5 = micro symbol 82 | "μs": int64(time.Microsecond), // U+03BC = Greek letter mu 83 | "ms": int64(time.Millisecond), 84 | "s": int64(time.Second), 85 | "m": int64(time.Minute), 86 | "h": int64(time.Hour), 87 | "d": int64(Day), 88 | "w": int64(Week), 89 | "mo": int64(Month), 90 | "y": int64(Year), 91 | } 92 | 93 | // ValidUnits returns the list of units the library supports. 94 | func ValidUnits() []string { 95 | return []string{ 96 | "ns", 97 | "us", 98 | "µs", 99 | "μs", 100 | "ms", 101 | "s", 102 | "m", 103 | "h", 104 | "d", 105 | "w", 106 | "mo", 107 | "y", 108 | } 109 | } 110 | 111 | // ParseDuration parses a duration string. 112 | // A duration string is a possibly signed sequence of 113 | // decimal numbers, each with optional fraction and a unit suffix, 114 | // such as "300ms", "-1.5h" or "2h45m". 115 | // 116 | // Restrictions: 117 | // - Days are always 24h. 118 | // - Weeks are always 7d (7 days). 119 | // - Months are always 30d (30 days). 120 | // - Years are always 365d (365 days). 121 | // 122 | // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h", "d", "w", "mo", "y". 123 | func Parse(s string) (time.Duration, error) { 124 | // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ 125 | orig := s 126 | var d int64 127 | neg := false 128 | 129 | // Consume [-+]? 130 | if s != "" { 131 | c := s[0] 132 | if c == '-' || c == '+' { 133 | neg = c == '-' 134 | s = s[1:] 135 | } 136 | } 137 | // Special case: if all that is left is "0", this is zero. 138 | if s == "0" { 139 | return 0, nil 140 | } 141 | if s == "" { 142 | return 0, errors.New("time: invalid duration " + quote(orig)) 143 | } 144 | for s != "" { 145 | var ( 146 | v, f int64 // integers before, after decimal point 147 | scale float64 = 1 // value = v + f/scale 148 | ) 149 | 150 | var err error 151 | 152 | // The next character must be [0-9.] 153 | if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { 154 | return 0, errors.New("time: invalid duration " + quote(orig)) 155 | } 156 | // Consume [0-9]* 157 | pl := len(s) 158 | v, s, err = leadingInt(s) 159 | if err != nil { 160 | return 0, errors.New("time: invalid duration " + quote(orig)) 161 | } 162 | pre := pl != len(s) // whether we consumed anything before a period 163 | 164 | // Consume (\.[0-9]*)? 165 | post := false 166 | if s != "" && s[0] == '.' { 167 | s = s[1:] 168 | pl := len(s) 169 | f, scale, s = leadingFraction(s) 170 | post = pl != len(s) 171 | } 172 | if !pre && !post { 173 | // no digits (e.g. ".s" or "-.s") 174 | return 0, errors.New("time: invalid duration " + quote(orig)) 175 | } 176 | 177 | // Consume unit. 178 | i := 0 179 | for ; i < len(s); i++ { 180 | c := s[i] 181 | if c == '.' || '0' <= c && c <= '9' { 182 | break 183 | } 184 | } 185 | if i == 0 { 186 | return 0, errors.New("time: missing unit in duration " + quote(orig)) 187 | } 188 | u := s[:i] 189 | s = s[i:] 190 | unit, ok := unitMap[u] 191 | if !ok { 192 | return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig) + ". Valid units are " + quotedUnitMapKeys()) 193 | } 194 | if v > (1<<63-1)/unit { 195 | // overflow 196 | return 0, errors.New("time: invalid duration " + quote(orig)) 197 | } 198 | v *= unit 199 | if f > 0 { 200 | // float64 is needed to be nanosecond accurate for fractions of hours. 201 | // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) 202 | v += int64(float64(f) * (float64(unit) / scale)) 203 | if v < 0 { 204 | // overflow 205 | return 0, errors.New("time: invalid duration " + quote(orig)) 206 | } 207 | } 208 | d += v 209 | if d < 0 { 210 | // overflow 211 | return 0, errors.New("time: invalid duration " + quote(orig)) 212 | } 213 | } 214 | 215 | if neg { 216 | d = -d 217 | } 218 | return time.Duration(d), nil 219 | } 220 | 221 | func quotedUnitMapKeys() string { 222 | var keys []string 223 | for _, k := range ValidUnits() { 224 | keys = append(keys, quote(k)) 225 | } 226 | return strings.Join(keys, ", ") 227 | } 228 | -------------------------------------------------------------------------------- /duration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the [Golang repository](https://github.com/golang/go/blob/master/LICENSE). 4 | 5 | package duration 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | ) 11 | 12 | var parseDurationTests = []struct { 13 | in string 14 | want time.Duration 15 | }{ 16 | // simple 17 | {"0", 0}, 18 | {"5s", 5 * time.Second}, 19 | {"30s", 30 * time.Second}, 20 | {"1478s", 1478 * time.Second}, 21 | {"10d", 10 * 24 * time.Hour}, 22 | {"2w", 14 * 24 * time.Hour}, 23 | {"1mo", 30 * 24 * time.Hour}, 24 | {"2y", 2 * 365 * 24 * time.Hour}, 25 | // sign 26 | {"-5s", -5 * time.Second}, 27 | {"+5s", 5 * time.Second}, 28 | {"-0", 0}, 29 | {"+0", 0}, 30 | // decimal 31 | {"5.0s", 5 * time.Second}, 32 | {"5.6s", 5*time.Second + 600*time.Millisecond}, 33 | {"5.s", 5 * time.Second}, 34 | {".5s", 500 * time.Millisecond}, 35 | {"1.0s", 1 * time.Second}, 36 | {"1.00s", 1 * time.Second}, 37 | {"1.004s", 1*time.Second + 4*time.Millisecond}, 38 | {"1.0040s", 1*time.Second + 4*time.Millisecond}, 39 | {"100.00100s", 100*time.Second + 1*time.Millisecond}, 40 | // different units 41 | {"10ns", 10 * time.Nanosecond}, 42 | {"11us", 11 * time.Microsecond}, 43 | {"12µs", 12 * time.Microsecond}, // U+00B5 44 | {"12μs", 12 * time.Microsecond}, // U+03BC 45 | {"13ms", 13 * time.Millisecond}, 46 | {"14s", 14 * time.Second}, 47 | {"15m", 15 * time.Minute}, 48 | {"16h", 16 * time.Hour}, 49 | // composite durations 50 | {"3h30m", 3*time.Hour + 30*time.Minute}, 51 | {"10.5s4m", 4*time.Minute + 10*time.Second + 500*time.Millisecond}, 52 | {"-2m3.4s", -(2*time.Minute + 3*time.Second + 400*time.Millisecond)}, 53 | {"1h2m3s4ms5us6ns", 1*time.Hour + 2*time.Minute + 3*time.Second + 4*time.Millisecond + 5*time.Microsecond + 6*time.Nanosecond}, 54 | {"39h9m14.425s", 39*time.Hour + 9*time.Minute + 14*time.Second + 425*time.Millisecond}, 55 | {"2w10d20h10m15s", (2*Week + 10*Day + 20*time.Hour + 10*time.Minute + 15*time.Second)}, 56 | // large value 57 | {"52763797000ns", 52763797000 * time.Nanosecond}, 58 | // more than 9 digits after decimal point, see https://golang.org/issue/6617 59 | {"0.3333333333333333333h", 20 * time.Minute}, 60 | // 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64 61 | {"9007199254740993ns", (1<<53 + 1) * time.Nanosecond}, 62 | // largest duration that can be represented by int64 in nanoseconds 63 | {"9223372036854775807ns", (1<<63 - 1) * time.Nanosecond}, 64 | {"9223372036854775.807us", (1<<63 - 1) * time.Nanosecond}, 65 | {"9223372036s854ms775us807ns", (1<<63 - 1) * time.Nanosecond}, 66 | // large negative value 67 | {"-9223372036854775807ns", -1<<63 + 1*time.Nanosecond}, 68 | // huge string; issue 15011. 69 | {"0.100000000000000000000h", 6 * time.Minute}, 70 | // This value tests the first overflow check in leadingFraction. 71 | {"0.830103483285477580700h", 49*time.Minute + 48*time.Second + 372539827*time.Nanosecond}, 72 | {"200y20mo", (200*Year + 20*Month)}, 73 | } 74 | 75 | func TestParseDuration(t *testing.T) { 76 | for _, tc := range parseDurationTests { 77 | d, err := Parse(tc.in) 78 | if err != nil || d != tc.want { 79 | t.Errorf("ParseDuration(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want) 80 | } 81 | } 82 | } 83 | 84 | func TestParseInvalidDuration(t *testing.T) { 85 | _, err := Parse("30x") 86 | if err == nil { 87 | t.Fatal("expected an error") 88 | } 89 | expect := `time: unknown unit "x" in duration "30x". Valid units are "ns", "us", "µs", "μs", "ms", "s", "m", "h", "d", "w", "mo", "y"` 90 | if err.Error() != expect { 91 | t.Errorf("ParseDuration(30x): %v, expected %v", err.Error(), expect) 92 | } 93 | } 94 | 95 | func TestKeysLen(t *testing.T) { 96 | k := len(ValidUnits()) 97 | m := len(unitMap) 98 | if k != m { 99 | t.Fatalf("unitKeys: %v, unitMap: %v, both should have the same lenght", k, m) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/caarlos0/duration 2 | 3 | go 1.16 4 | --------------------------------------------------------------------------------