Custom implementation of marshal, unmarshal and string methods.
package json
import (
"fmt"
"strings"
"time"
)
const rfc3339 string = "2006-01-02"
// Date represents a date without a time component, encoded as a string
// in the "YYYY-MM-DD" format.
type Date struct {
Year int
Month time.Month
Day int
}
// UnmarshalJSON implements json.Unmarshaler inferface.
func (d *Date) UnmarshalJSON(b []byte) error {
t, err := time.Parse(rfc3339, strings.Trim(string(b), `"`))
if err != nil {
return err
}
d.Year, d.Month, d.Day = t.Date()
return nil
}
// MarshalJSON implements json.Marshaler interface.
func (d Date) MarshalJSON() ([]byte, error) {
s := fmt.Sprintf(`"%04d-%02d-%02d"`, d.Year, d.Month, d.Day)
return []byte(s), nil
}
// String defines a string representation.
// It will be called automatically when you try to convert struct instance
// to a string.
func (d Date) String() string {
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}
And tests for them.
package json
import (
"encoding/json"
"testing"
"time"
)
func TestDate_UnmarshalJSON(t *testing.T) {
in := `"2022-12-31"`
want := time.Date(2022, time.December, 31, 0, 0, 0, 0, time.UTC)
var got Date
if err := got.UnmarshalJSON([]byte(in)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !(got.Year == want.Year() && got.Month == want.Month() && got.Day == want.Day()) {
t.Errorf("got date = %s, want %s", got, want)
}
}
func TestDate_UnmarshalJSON_badFormat(t *testing.T) {
in := `"31 Dec 22"`
var got Date
err := got.UnmarshalJSON([]byte(in))
if err, ok := err.(*time.ParseError); !ok {
t.Errorf("expected a time parse error, got: %v", err)
}
}
func TestDate_MarshalJSON(t *testing.T) {
testcases := map[string]struct {
in Date
want string
}{
"without zero padding": {
in: Date{2022, time.December, 31},
want: `"2022-12-31"`,
},
"with zero padding": {
in: Date{2022, time.July, 1},
want: `"2022-07-01"`,
},
"initial value": {
in: Date{},
want: `"0000-00-00"`,
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
got, err := json.Marshal(tc.in)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(got) != tc.want {
t.Errorf("got date = %s, want %s", got, tc.want)
}
})
}
}
func TestDate_String(t *testing.T) {
testcases := map[string]struct {
in Date
want string
}{
"without zero padding": {
in: Date{2022, time.December, 31},
want: "2022-12-31",
},
"with zero padding": {
in: Date{2022, time.July, 1},
want: "2022-07-01",
},
"initial value": {
in: Date{},
want: "0000-00-00",
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
if got := tc.in.String(); got != tc.want {
t.Errorf("got %q, want %q", got, tc.want)
}
})
}
}