Commit f298d0ce authored by David Symonds's avatar David Symonds

time: add ParseDuration.

R=rsc, r, r
CC=golang-dev
https://golang.org/cl/5489111
parent e6a322b0
......@@ -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
}
......@@ -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()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment