]> Cypherpunks repositories - gostls13.git/commitdiff
time: add ParseDuration.
authorDavid Symonds <dsymonds@golang.org>
Fri, 23 Dec 2011 05:28:56 +0000 (16:28 +1100)
committerDavid Symonds <dsymonds@golang.org>
Fri, 23 Dec 2011 05:28:56 +0000 (16:28 +1100)
R=rsc, r, r
CC=golang-dev
https://golang.org/cl/5489111

src/pkg/time/format.go
src/pkg/time/time_test.go

index 082a51a1621ddac36123841b8c5e7e40eb8b6249..cbcde5b637b1489d0bc6687bc373fd7d6623b571 100644 (file)
@@ -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
+}
index 484ae4266a31e3d0a190cdde724f327350c28f29..cdc1c39c5f535e62380d35a167fd8e8c88b5fcc9 100644 (file)
@@ -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()