From 86146853cce01a08ead23608ad8021f87bf4464d Mon Sep 17 00:00:00 2001 From: Seth Shelnutt Date: Thu, 23 Jun 2016 08:17:28 -0400 Subject: [PATCH] Add support for marshal/unmarshal to time.Duration. #16039 This add support for MarshalJSON, MarshalText and MarshalBinary to the time.Duration type. --- src/time/time.go | 112 +++++++++++++++++++++++++++++++++++ src/time/time_test.go | 133 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 239 insertions(+), 6 deletions(-) diff --git a/src/time/time.go b/src/time/time.go index d9dbd3449a04d..e5ea225026802 100644 --- a/src/time/time.go +++ b/src/time/time.go @@ -603,6 +603,118 @@ func (d Duration) Hours() float64 { return float64(hour) + float64(nsec)*(1e-9/60/60) } +const durationBinaryVersion byte = 1 + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Duration) MarshalBinary() ([]byte, error) { + /* + // Create a byte buffer to store int64 into + var buf = make([]byte, binary.MaxVarintLen64) + digitsStored := binary.PutVarint(buf, int64(d)) + + // Validate at least one digit was stored + if digitsStored < 1 { + return nil, errors.New("Duration.MarshalBinary: could not stored digits in binary") + } + + enc := []byte{ + durationBinaryVersion, // byte 0 : version + } + + enc = append(enc, buf...) + */ + + enc := []byte{ + durationBinaryVersion, // byte 0 : version + byte(d >> 56), // bytes 1-8: nanoseconds + byte(d >> 48), + byte(d >> 40), + byte(d >> 32), + byte(d >> 24), + byte(d >> 16), + byte(d >> 8), + byte(d), + } + + return enc, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (d *Duration) UnmarshalBinary(data []byte) error { + buf := data + if len(buf) == 0 { + return errors.New("Duration.UnmarshalBinary: no data") + } + + if buf[0] != durationBinaryVersion { + return errors.New("Duration.UnmarshalBinary: unsupported version") + } + + if len(buf) != /*version*/ 1+ /*nanoseconds*/ 8 { + return errors.New("Duration.UnmarshalBinary: invalid length") + } + + buf = buf[1:] + *d = Duration(int64(buf[7]) | int64(buf[6])<<8 | int64(buf[5])<<16 | int64(buf[4])<<24 | + int64(buf[3])<<32 | int64(buf[2])<<40 | int64(buf[1])<<48 | int64(buf[0])<<56) + /* + // Parse the bytes to an int64 + convertedInt64, _ := binary.Varint(buf) + // Convert the int64 to Duration type + *d = Duration(convertedInt64) + */ + return nil +} + +// TODO(rsc): Remove GobEncoder, GobDecoder, MarshalJSON, UnmarshalJSON in Go 2. +// The same semantics will be provided by the generic MarshalBinary, MarshalText, +// UnmarshalBinary, UnmarshalText. + +// GobEncode implements the gob.GobEncoder interface. +func (d Duration) GobEncode() ([]byte, error) { + return d.MarshalBinary() +} + +// GobDecode implements the gob.GobDecoder interface. +func (d *Duration) GobDecode(data []byte) error { + return d.UnmarshalBinary(data) +} + +// MarshalJSON implements the json.Marshaler interface. +// The duration is a quoted string provided by String() +func (d Duration) MarshalJSON() ([]byte, error) { + + b := make([]byte, 0, len(RFC3339Nano)+2) + b = append(b, '"') + b = append(b, []byte(d.String())...) + b = append(b, '"') + return b, nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// The duration is expected to be a quoted string in ParseDuration syntax. +func (d *Duration) UnmarshalJSON(data []byte) error { + // Fractional seconds are handled implicitly by Parse. + var err error + *d, err = ParseDuration(string(data)[1 : len(string(data))-1]) + return err +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The duration is a quoted string provided by String() +func (d Duration) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The duration is expected to be a quoted string in ParseDuration syntax. +func (d *Duration) UnmarshalText(data []byte) error { + // Fractional seconds are handled implicitly by Parse. + var err error + *d, err = ParseDuration(string(data)) + return err +} + // Add returns the time t+d. func (t Time) Add(d Duration) Time { t.sec += int64(d / 1e9) diff --git a/src/time/time_test.go b/src/time/time_test.go index b7ebb37296716..11782dad5219c 100644 --- a/src/time/time_test.go +++ b/src/time/time_test.go @@ -665,7 +665,7 @@ func equalTimeAndZone(a, b Time) bool { return a.Equal(b) && aoffset == boffset && aname == bname } -var gobTests = []Time{ +var gobTimeTests = []Time{ Date(0, 1, 2, 3, 4, 5, 6, UTC), Date(7, 8, 9, 10, 11, 12, 13, FixedZone("", 0)), Unix(81985467080890095, 0x76543210), // Time.sec: 0x0123456789ABCDEF @@ -678,7 +678,7 @@ func TestTimeGob(t *testing.T) { var b bytes.Buffer enc := gob.NewEncoder(&b) dec := gob.NewDecoder(&b) - for _, tt := range gobTests { + for _, tt := range gobTimeTests { var gobtt Time if err := enc.Encode(&tt); err != nil { t.Errorf("%v gob Encode error = %q, want nil", tt, err) @@ -691,7 +691,7 @@ func TestTimeGob(t *testing.T) { } } -var invalidEncodingTests = []struct { +var invalidTimeEncodingTests = []struct { bytes []byte want string }{ @@ -701,7 +701,7 @@ var invalidEncodingTests = []struct { } func TestInvalidTimeGob(t *testing.T) { - for _, tt := range invalidEncodingTests { + for _, tt := range invalidTimeEncodingTests { var ignored Time err := ignored.GobDecode(tt.bytes) if err == nil || err.Error() != tt.want { @@ -737,7 +737,7 @@ func TestNotGobEncodableTime(t *testing.T) { } } -var jsonTests = []struct { +var jsonTimeTests = []struct { time Time json string }{ @@ -747,7 +747,7 @@ var jsonTests = []struct { } func TestTimeJSON(t *testing.T) { - for _, tt := range jsonTests { + for _, tt := range jsonTimeTests { var jsonTime Time if jsonBytes, err := json.Marshal(tt.time); err != nil { @@ -883,6 +883,127 @@ func TestParseDurationRoundTrip(t *testing.T) { } } +var gobDurationTests = []Duration{ + Duration(123456789), + Duration(rand.Int31()) * Millisecond, + 16 * Hour, + 4*Minute + 10*Second + 500*Millisecond, + 0, + -1<<63 + 1*Nanosecond, +} + +func TestDurationGob(t *testing.T) { + var b bytes.Buffer + enc := gob.NewEncoder(&b) + dec := gob.NewDecoder(&b) + for _, dt := range gobDurationTests { + var gobdt Duration + if err := enc.Encode(&dt); err != nil { + t.Errorf("%v gob Encode error = %q, want nil", dt, err) + } else if err := dec.Decode(&gobdt); err != nil { + t.Errorf("%v gob Decode error = %q, want nil", dt, err) + } else if gobdt != dt { + t.Errorf("Decoded duration = %v, want %v", gobdt, dt) + } + b.Reset() + } +} + +var invalidDurationEncodingTests = []struct { + bytes []byte + want string +}{ + {[]byte{}, "Duration.UnmarshalBinary: no data"}, + {[]byte{0, 2, 3}, "Duration.UnmarshalBinary: unsupported version"}, + {[]byte{1, 2, 3}, "Duration.UnmarshalBinary: invalid length"}, +} + +func TestInvalidDurationGob(t *testing.T) { + for _, tt := range invalidDurationEncodingTests { + var ignored Duration + err := ignored.GobDecode(tt.bytes) + if err == nil || err.Error() != tt.want { + t.Errorf("time.GobDecode(%#v) error = %v, want %v", tt.bytes, err, tt.want) + } + err = ignored.UnmarshalBinary(tt.bytes) + if err == nil || err.Error() != tt.want { + t.Errorf("time.UnmarshalBinary(%#v) error = %v, want %v", tt.bytes, err, tt.want) + } + } +} + +var jsonDurationTests = []struct { + json string + ok bool + duration Duration +}{ + // simple + {`"0s"`, true, 0}, + {`"5s"`, true, 5 * Second}, + {`"30s"`, true, 30 * Second}, + {`"24m38s"`, true, 1478 * Second}, + // sign + {`"-5s"`, true, -5 * Second}, + {`"5s"`, true, 5 * Second}, + // decimal + {`"5s"`, true, 5 * Second}, + {`"5.6s"`, true, 5*Second + 600*Millisecond}, + {`"500ms"`, true, 500 * Millisecond}, + {`"1s"`, true, 1 * Second}, + {`"1s"`, true, 1 * Second}, + {`"1.004s"`, true, 1*Second + 4*Millisecond}, + {`"1.004s"`, true, 1*Second + 4*Millisecond}, + {`"1m40.001s"`, true, 100*Second + 1*Millisecond}, + // different units + {`"10ns"`, true, 10 * Nanosecond}, + {`"11µs"`, true, 11 * Microsecond}, + {`"12µs"`, true, 12 * Microsecond}, // U+00B5 + {`"13ms"`, true, 13 * Millisecond}, + {`"14s"`, true, 14 * Second}, + {`"15m0s"`, true, 15 * Minute}, + {`"16h0m0s"`, true, 16 * Hour}, + // composite durations + {`"3h30m0s"`, true, 3*Hour + 30*Minute}, + {`"4m10.5s"`, true, 4*Minute + 10*Second + 500*Millisecond}, + {`"-2m3.4s"`, true, -(2*Minute + 3*Second + 400*Millisecond)}, + {`"1h2m3.004005006s"`, true, 1*Hour + 2*Minute + 3*Second + 4*Millisecond + 5*Microsecond + 6*Nanosecond}, + {`"39h9m14.425s"`, true, 39*Hour + 9*Minute + 14*Second + 425*Millisecond}, + // large value + {`"52.763797s"`, true, 52763797000 * Nanosecond}, + // more than 9 digits after decimal point, see https://golang.org/issue/6617 + {`"20m0s"`, true, 20 * Minute}, + // 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64 + {`"2501h59m59.254740993s"`, true, (1<<53 + 1) * Nanosecond}, + // largest duration that can be represented by int64 in nanoseconds + {`"2562047h47m16.854775807s"`, true, (1<<63 - 1) * Nanosecond}, + // large negative value + {`"-2562047h47m16.854775807s"`, true, -1<<63 + 1*Nanosecond}, +} + +func TestDurationJSON(t *testing.T) { + for _, dt := range jsonDurationTests { + var jsonDuration Duration + + if jsonBytes, err := json.Marshal(dt.duration); err != nil { + t.Errorf("%v json.Marshal error = %v, want nil", dt.duration, err) + } else if string(jsonBytes) != dt.json { + t.Errorf("%v JSON = %#q, want %#q", dt.duration, string(jsonBytes), dt.json) + } else if err = json.Unmarshal(jsonBytes, &jsonDuration); err != nil { + t.Errorf("%v json.Unmarshal error = %v, want nil", dt.duration, err) + } else if jsonDuration != dt.duration { + t.Errorf("Unmarshaled duration = %v, want %v", jsonDuration, dt.duration) + } + } +} + +func TestInvalidDurationJSON(t *testing.T) { + var dt Duration + err := json.Unmarshal([]byte(`{"now is the duration":"buddy"}`), &dt) + if err == nil { + t.Errorf("expected *Error unmarshaling JSON, got %v", err) + } +} + // golang.org/issue/4622 func TestLocationRace(t *testing.T) { ResetLocalOnceForTest() // reset the Once to trigger the race