]> Cypherpunks repositories - gostls13.git/commitdiff
time: JSON marshaler for Time
authorRobert Hencke <robert.hencke@gmail.com>
Tue, 20 Dec 2011 17:01:18 +0000 (09:01 -0800)
committerRob Pike <r@golang.org>
Tue, 20 Dec 2011 17:01:18 +0000 (09:01 -0800)
R=golang-dev, dsymonds, hectorchu, r, r
CC=golang-dev
https://golang.org/cl/5496064

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

index 8e24daeff73d309a2c2436c1ebf499c487e83ebf..33d557f7369b8ef792f4ddf99727bdfcca169310 100644 (file)
@@ -7,6 +7,8 @@
 // The calendrical calculations always assume a Gregorian calendar.
 package time
 
+import "errors"
+
 // A Time represents an instant in time with nanosecond precision.
 //
 // Programs using times should typically store and pass them as values,
@@ -765,11 +767,11 @@ func (t Time) GobEncode() ([]byte, error) {
        } else {
                _, offset := t.Zone()
                if offset%60 != 0 {
-                       return nil, gobError("Time.GobEncode: zone offset has fractional minute")
+                       return nil, errors.New("Time.GobEncode: zone offset has fractional minute")
                }
                offset /= 60
                if offset < -32768 || offset == -1 || offset > 32767 {
-                       return nil, gobError("Time.GobEncode: unexpected zone offset")
+                       return nil, errors.New("Time.GobEncode: unexpected zone offset")
                }
                offsetMin = int16(offset)
        }
@@ -798,15 +800,15 @@ func (t Time) GobEncode() ([]byte, error) {
 // GobDecode implements the gob.GobDecoder interface.
 func (t *Time) GobDecode(buf []byte) error {
        if len(buf) == 0 {
-               return gobError("Time.GobDecode: no data")
+               return errors.New("Time.GobDecode: no data")
        }
 
        if buf[0] != timeGobVersion {
-               return gobError("Time.GobDecode: unsupported version")
+               return errors.New("Time.GobDecode: unsupported version")
        }
 
        if len(buf) != /*version*/ 1+ /*sec*/ 8+ /*nsec*/ 4+ /*zone offset*/ 2 {
-               return gobError("Time.GobDecode: invalid length")
+               return errors.New("Time.GobDecode: invalid length")
        }
 
        buf = buf[1:]
@@ -830,6 +832,52 @@ func (t *Time) GobDecode(buf []byte) error {
        return nil
 }
 
+// MarshalJSON implements the json.Marshaler interface.
+// Time is formatted as RFC3339.
+func (t Time) MarshalJSON() ([]byte, error) {
+       yearInt := t.Year()
+       if yearInt < 0 || yearInt > 9999 {
+               return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
+       }
+
+       // We need a four-digit year, but Format produces variable-width years.
+       year := itoa(yearInt)
+       year = "0000"[:4-len(year)] + year
+
+       var formattedTime string
+       if t.nsec == 0 {
+               // RFC3339, no fractional second
+               formattedTime = t.Format("-01-02T15:04:05Z07:00")
+       } else {
+               // RFC3339 with fractional second
+               formattedTime = t.Format("-01-02T15:04:05.000000000Z07:00")
+
+               // Trim trailing zeroes from fractional second.
+               const nanoEnd = 24 // Index of last digit of fractional second
+               var i int
+               for i = nanoEnd; formattedTime[i] == '0'; i-- {
+                       // Seek backwards until first significant digit is found.
+               }
+
+               formattedTime = formattedTime[:i+1] + formattedTime[nanoEnd+1:]
+       }
+
+       buf := make([]byte, 0, 1+len(year)+len(formattedTime)+1)
+       buf = append(buf, '"')
+       buf = append(buf, year...)
+       buf = append(buf, formattedTime...)
+       buf = append(buf, '"')
+       return buf, nil
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+// Time is expected in RFC3339 format.
+func (t *Time) UnmarshalJSON(data []byte) (err error) {
+       *t, err = Parse("\""+RFC3339+"\"", string(data))
+       // Fractional seconds are handled implicitly by Parse.
+       return
+}
+
 // Unix returns the local Time corresponding to the given Unix time,
 // sec seconds and nsec nanoseconds since January 1, 1970 UTC.
 // It is valid to pass nsec outside the range [0, 999999999].
index bcc9c42365d153401a4f83633afbd37b28c7542e..484ae4266a31e3d0a190cdde724f327350c28f29 100644 (file)
@@ -7,6 +7,7 @@ package time_test
 import (
        "bytes"
        "encoding/gob"
+       "encoding/json"
        "strconv"
        "strings"
        "testing"
@@ -694,6 +695,12 @@ func TestAddToExactSecond(t *testing.T) {
        }
 }
 
+func equalTimeAndZone(a, b Time) bool {
+       aname, aoffset := a.Zone()
+       bname, boffset := b.Zone()
+       return a.Equal(b) && aoffset == boffset && aname == bname
+}
+
 var gobTests = []Time{
        Date(0, 1, 2, 3, 4, 5, 6, UTC),
        Date(7, 8, 9, 10, 11, 12, 13, FixedZone("", 0)),
@@ -713,12 +720,8 @@ func TestTimeGob(t *testing.T) {
                        t.Errorf("%v gob Encode error = %q, want nil", tt, err)
                } else if err := dec.Decode(&gobtt); err != nil {
                        t.Errorf("%v gob Decode error = %q, want nil", tt, err)
-               } else {
-                       gobname, goboffset := gobtt.Zone()
-                       name, offset := tt.Zone()
-                       if !gobtt.Equal(tt) || goboffset != offset || gobname != name {
-                               t.Errorf("Decoded time = %v, want %v", gobtt, tt)
-                       }
+               } else if !equalTimeAndZone(gobtt, tt) {
+                       t.Errorf("Decoded time = %v, want %v", gobtt, tt)
                }
                b.Reset()
        }
@@ -762,6 +765,57 @@ func TestNotGobEncodableTime(t *testing.T) {
        }
 }
 
+var jsonTests = []struct {
+       time Time
+       json string
+}{
+       {Date(9999, 4, 12, 23, 20, 50, .52*1e9, UTC), `"9999-04-12T23:20:50.52Z"`},
+       {Date(1996, 12, 19, 16, 39, 57, 0, Local), `"1996-12-19T16:39:57-08:00"`},
+       {Date(0, 1, 1, 0, 0, 0, 1, FixedZone("", 1*60)), `"0000-01-01T00:00:00.000000001+00:01"`},
+}
+
+func TestTimeJSON(t *testing.T) {
+       for _, tt := range jsonTests {
+               var jsonTime Time
+
+               if jsonBytes, err := json.Marshal(tt.time); err != nil {
+                       t.Errorf("%v json.Marshal error = %v, want nil", tt.time, err)
+               } else if string(jsonBytes) != tt.json {
+                       t.Errorf("%v JSON = %q, want %q", tt.time, string(jsonBytes), tt.json)
+               } else if err = json.Unmarshal(jsonBytes, &jsonTime); err != nil {
+                       t.Errorf("%v json.Unmarshal error = %v, want nil", tt.time, err)
+               } else if !equalTimeAndZone(jsonTime, tt.time) {
+                       t.Errorf("Unmarshaled time = %v, want %v", jsonTime, tt.time)
+               }
+       }
+}
+
+func TestInvalidTimeJSON(t *testing.T) {
+       var tt Time
+       err := json.Unmarshal([]byte(`{"now is the time":"buddy"}`), &tt)
+       _, isParseErr := err.(*ParseError)
+       if !isParseErr {
+               t.Errorf("expected *time.ParseError unmarshaling JSON, got %v", err)
+       }
+}
+
+var notJSONEncodableTimes = []struct {
+       time Time
+       want string
+}{
+       {Date(10000, 1, 1, 0, 0, 0, 0, UTC), "Time.MarshalJSON: year outside of range [0,9999]"},
+       {Date(-1, 1, 1, 0, 0, 0, 0, UTC), "Time.MarshalJSON: year outside of range [0,9999]"},
+}
+
+func TestNotJSONEncodableTime(t *testing.T) {
+       for _, tt := range notJSONEncodableTimes {
+               _, err := tt.time.MarshalJSON()
+               if err == nil || err.Error() != tt.want {
+                       t.Errorf("%v MarshalJSON error = %v, want %v", tt.time, err, tt.want)
+               }
+       }
+}
+
 func BenchmarkNow(b *testing.B) {
        for i := 0; i < b.N; i++ {
                Now()