From f298d0ce29cdd6a3521cb6a062a9fc4a104392fc Mon Sep 17 00:00:00 2001 From: David Symonds Date: Fri, 23 Dec 2011 16:28:56 +1100 Subject: [PATCH] time: add ParseDuration. R=rsc, r, r CC=golang-dev https://golang.org/cl/5489111 --- src/pkg/time/format.go | 146 +++++++++++++++++++++++++++++++++----- src/pkg/time/time_test.go | 77 ++++++++++++++++++++ 2 files changed, 207 insertions(+), 16 deletions(-) diff --git a/src/pkg/time/format.go b/src/pkg/time/format.go index 082a51a1621..cbcde5b637b 100644 --- a/src/pkg/time/format.go +++ b/src/pkg/time/format.go @@ -283,25 +283,16 @@ var atoiError = errors.New("time: invalid number") // Duplicates functionality in strconv, but avoids dependency. func atoi(s string) (x int, err error) { - i := 0 - if len(s) > 0 && s[0] == '-' { - i++ + neg := false + if s != "" && s[0] == '-' { + neg = true + s = s[1:] } - if i >= len(s) { + x, rem, err := leadingInt(s) + if err != nil || rem != "" { return 0, atoiError } - for ; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - return 0, atoiError - } - if x >= (1<<31-10)/10 { - // will overflow - return 0, atoiError - } - x = x*10 + int(c) - '0' - } - if s[0] == '-' { + if neg { x = -x } return x, nil @@ -893,3 +884,126 @@ func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, } return } + +var errLeadingInt = errors.New("time: bad [0-9]*") // never printed + +// leadingInt consumes the leading [0-9]* from s. +func leadingInt(s string) (x int, rem string, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x >= (1<<31-10)/10 { + // overflow + return 0, "", errLeadingInt + } + x = x*10 + int(c) - '0' + } + return x, s[i:], nil +} + +var unitMap = map[string]float64{ + "ns": float64(Nanosecond), + "us": float64(Microsecond), + "µs": float64(Microsecond), // U+00B5 = micro symbol + "μs": float64(Microsecond), // U+03BC = Greek letter mu + "ms": float64(Millisecond), + "s": float64(Second), + "m": float64(Minute), + "h": float64(Hour), +} + +// 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". +func ParseDuration(s string) (Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + orig := s + f := float64(0) + 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 " + orig) + } + for s != "" { + g := float64(0) // this element of the sequence + + var x int + 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 " + orig) + } + // Consume [0-9]* + pl := len(s) + x, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("time: invalid duration " + orig) + } + g = float64(x) + 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) + x, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("time: invalid duration " + orig) + } + scale := 1 + for n := pl - len(s); n > 0; n-- { + scale *= 10 + } + g += float64(x) / float64(scale) + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("time: invalid duration " + 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 " + orig) + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("time: unknown unit " + u + " in duration " + orig) + } + + f += g * unit + } + + if neg { + f = -f + } + return Duration(f), nil +} diff --git a/src/pkg/time/time_test.go b/src/pkg/time/time_test.go index 484ae4266a3..cdc1c39c5f5 100644 --- a/src/pkg/time/time_test.go +++ b/src/pkg/time/time_test.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/gob" "encoding/json" + "math/rand" "strconv" "strings" "testing" @@ -816,6 +817,82 @@ func TestNotJSONEncodableTime(t *testing.T) { } } +var parseDurationTests = []struct { + in string + ok bool + want Duration +}{ + // simple + {"0", true, 0}, + {"5s", true, 5 * Second}, + {"30s", true, 30 * Second}, + {"1478s", true, 1478 * Second}, + // sign + {"-5s", true, -5 * Second}, + {"+5s", true, 5 * Second}, + {"-0", true, 0}, + {"+0", true, 0}, + // decimal + {"5.0s", true, 5 * Second}, + {"5.6s", true, 5*Second + 600*Millisecond}, + {"5.s", true, 5 * Second}, + {".5s", true, 500 * Millisecond}, + {"1.0s", true, 1 * Second}, + {"1.00s", true, 1 * Second}, + {"1.004s", true, 1*Second + 4*Millisecond}, + {"1.0040s", true, 1*Second + 4*Millisecond}, + {"100.00100s", true, 100*Second + 1*Millisecond}, + // different units + {"10ns", true, 10 * Nanosecond}, + {"11us", true, 11 * Microsecond}, + {"12µs", true, 12 * Microsecond}, // U+00B5 + {"12μs", true, 12 * Microsecond}, // U+03BC + {"13ms", true, 13 * Millisecond}, + {"14s", true, 14 * Second}, + {"15m", true, 15 * Minute}, + {"16h", true, 16 * Hour}, + // composite durations + {"3h30m", true, 3*Hour + 30*Minute}, + {"10.5s4m", true, 4*Minute + 10*Second + 500*Millisecond}, + {"-2m3.4s", true, -(2*Minute + 3*Second + 400*Millisecond)}, + {"1h2m3s4ms5us6ns", true, 1*Hour + 2*Minute + 3*Second + 4*Millisecond + 5*Microsecond + 6*Nanosecond}, + {"39h9m14.425s", true, 39*Hour + 9*Minute + 14*Second + 425*Millisecond}, + + // errors + {"", false, 0}, + {"3", false, 0}, + {"-", false, 0}, + {"s", false, 0}, + {".", false, 0}, + {"-.", false, 0}, + {".s", false, 0}, + {"+.s", false, 0}, +} + +func TestParseDuration(t *testing.T) { + for _, tc := range parseDurationTests { + d, err := ParseDuration(tc.in) + if tc.ok && (err != nil || d != tc.want) { + t.Errorf("ParseDuration(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want) + } else if !tc.ok && err == nil { + t.Errorf("ParseDuration(%q) = _, nil, want _, non-nil", tc.in) + } + } +} + +func TestParseDurationRoundTrip(t *testing.T) { + for i := 0; i < 100; i++ { + // Resolutions finer than milliseconds will result in + // imprecise round-trips. + d0 := Duration(rand.Int31()) * Millisecond + s := d0.String() + d1, err := ParseDuration(s) + if err != nil || d0 != d1 { + t.Errorf("round-trip failed: %d => %q => %d, %v", d0, s, d1, err) + } + } +} + func BenchmarkNow(b *testing.B) { for i := 0; i < b.N; i++ { Now()