struct ↔ csv 部分

csv パーステスト

type TestCSV struct {
	Id      int                           `label:"ID" json:"ID,string"`
	Title   string                        `label:"タイトル" json:"タイトル"`
	Done    bool                          `label:"終了したか" json:"終了したか,string"`
	Tm      *util.JSONableTargetMonthTime `label:"対象月" json:"対象月"`
	Created *util.JSONableYmdHisTime      `label:"作成日" json:"作成日"`
}

func TestStructToCSV(t *testing.T) {
	tests := []struct {
		title       string
		st          interface{}
		expectedCSV string
		expectedErr error
	}{
		{
			"成功",
			TestCSV{
				1, "テスト", false,
				util.NewJSONableTargetMonthTime("200802"),
				util.NewJSONableYmdHisTime("2019/10/11 12:23:34"),
			},
			`1,テスト,false,200802,2019/10/11 12:23:34`,
			nil,
		},
		{"struct ではない", "hoge", "", errors.New("util: StructToCSV failed. %!t(string=hoge) is not struct")},
	}
	for _, tt := range tests {
		t.Run(tt.title, func(t *testing.T) {
			actual, err := util.StructToCSV(tt.st)
			assert.Equal(t, tt.expectedErr, err)
			assert.Equal(t, tt.expectedCSV, actual)
		})
	}
}

func TestSliceToCSV(t *testing.T) {
	tests := []struct {
		title       string
		sl          interface{}
		expectedCSV string
		expectedErr error
	}{
		{
			"成功",
			[]TestCSV{
				{
					1, "テスト1", false,
					util.NewJSONableTargetMonthTime("200802"),
					util.NewJSONableYmdHisTime("2019/01/02 03:04:05"),
				},
				{
					2, "テスト2", true,
					util.NewJSONableTargetMonthTime("200903"),
					util.NewJSONableYmdHisTime("2020/02/03 05:07:09"),
				},
				{
					2, "テスト3", false,
					util.NewJSONableTargetMonthTime("201004"),
					util.NewJSONableYmdHisTime("2021/03/04 06:08:20"),
				},
			},
			`ID,タイトル,終了したか,対象月,作成日
1,テスト1,false,200802,2019/01/02 03:04:05
2,テスト2,true,200903,2020/02/03 05:07:09
2,テスト3,false,201004,2021/03/04 06:08:20`,
			nil,
		},
		{"slice ではない", "hoge", "", errors.New("util: SliceToCSV failed. %!t(string=hoge) is not struct slice")},
		{"要素が struct ではない", []int{1, 2}, "", errors.New("util: SliceToCSV failed. [%!t(int=1) %!t(int=2)] is not struct slice")},
    {
			"要素数が 0",
			[]TestCSV{},
			`ID,タイトル,終了したか,対象月,作成日`,
			nil,
		},
	}
	for _, tt := range tests {
		t.Run(tt.title, func(t *testing.T) {
			actual, err := util.SliceToCSV(tt.sl)
			assert.Equal(t, tt.expectedErr, err)
			assert.Equal(t, tt.expectedCSV, actual)
		})
	}
}

func TestCSVToSlice(t *testing.T) {
	tests := []struct {
		title       string
		csv         string
		expectedSL  interface{}
		expectedErr error
	}{
		{
			"成功",
			`ID,タイトル,終了したか,対象月,作成日
1,テスト1,false,200802,2019/01/02 03:04:05
2,テスト2,true,200903,2020/02/03 05:07:09
2,テスト3,false,201004,2021/03/04 06:08:20`,
			[]TestCSV{
				{
					1, "テスト1", false,
					util.NewJSONableTargetMonthTime("200802"),
					util.NewJSONableYmdHisTime("2019/01/02 03:04:05"),
				},
				{
					2, "テスト2", true,
					util.NewJSONableTargetMonthTime("200903"),
					util.NewJSONableYmdHisTime("2020/02/03 05:07:09"),
				},
				{
					2, "テスト3", false,
					util.NewJSONableTargetMonthTime("201004"),
					util.NewJSONableYmdHisTime("2021/03/04 06:08:20"),
				},
			},
			nil,
		},
	}
	for _, tt := range tests {
		t.Run(tt.title, func(t *testing.T) {
			actual := []TestCSV{}
			err := util.CSVToSlice(tt.csv, &actual)
			assert.Equal(t, tt.expectedErr, err)
			assert.Equal(t, tt.expectedSL, actual)
		})
	}
}

json 用の time の用意

// formats
const (
	Ym          = "200601"
	YmdHisSlash = "2006/01/02 15:04:05"
)

// JSONableTime json time
// デフォルトの time.Time の Unmarshal が time.RFC3339 基準なので独自に定義したもの
type JSONableTime interface {
	Time() time.Time
	Format() string

	json.Marshaler
	json.Unmarshaler
}

// JSONableTargetMonthTime Ym の対象月形式
type JSONableTargetMonthTime struct {
	JSONableTime

	time time.Time
}

// NewJSONableTargetMonthTime return JSONableTargetMonthTime
func NewJSONableTargetMonthTime(t string) *JSONableTargetMonthTime {
	tm, err := time.Parse(Ym, t)
	if err != nil {
		panic(err)
	}
	return &JSONableTargetMonthTime{
		time: tm,
	}
}

// Time return time.Time
func (t *JSONableTargetMonthTime) Time() time.Time {
	return t.time
}

// Format format Ym
func (t *JSONableTargetMonthTime) Format() string {
	return t.time.Format(Ym)
}

// MarshalJSON implements json.Marshaler
func (t *JSONableTargetMonthTime) MarshalJSON() ([]byte, error) {
	if t == nil {
		return nil, nil
	}
	// syntax error にはならないが, target month は string として扱いたいため json.Marshal
	return json.Marshal(t.time.Format(Ym))
}

// UnmarshalJSON implements json.Unmarshaler
func (t *JSONableTargetMonthTime) UnmarshalJSON(data []byte) error {
	if t == nil {
		return nil
	}
	if strings.ToLower(string(data)) == "null" {
		return nil
	}
	// json.Marshal されてる前提のため Unmarshal
	str := ""
	if err := json.Unmarshal(data, &str); err != nil {
		return err
	}
	var err error
	t.time, err = time.Parse(Ym, str)
	return err
}

// JSONableYmdHisTime Y/m/d H:i:s の日時形式
type JSONableYmdHisTime struct {
	JSONableTime

	time time.Time
}

// NewJSONableYmdHisTime return JSONableYmdHisTime
func NewJSONableYmdHisTime(t string) *JSONableYmdHisTime {
	tm, err := time.Parse(YmdHisSlash, t)
	if err != nil {
		panic(err)
	}
	return &JSONableYmdHisTime{
		time: tm,
	}
}

// Time return time.Time
func (t *JSONableYmdHisTime) Time() time.Time {
	return t.time
}

// Format format Ym
func (t *JSONableYmdHisTime) Format() string {
	return t.time.Format(YmdHisSlash)
}

// MarshalJSON implements json.Marshaler
func (t *JSONableYmdHisTime) MarshalJSON() ([]byte, error) {
	if t == nil {
		return nil, nil
	}
	// json.Marshal を介さない場合, "" で括ってあげないと json.SyntaxError になる
	return json.Marshal(t.time.Format(YmdHisSlash))
}

// UnmarshalJSON implements json.Unmarshaler
func (t *JSONableYmdHisTime) UnmarshalJSON(data []byte) error {
	if t == nil {
		return nil
	}
	str := ""
	// Marshal 時と同様の理由で "" で括られているものをパースしてあげる必要があるため Unmarshal
	if err := json.Unmarshal(data, &str); err != nil {
		return err
	}
	if strings.ToLower(str) == "null" {
		return nil
	}
	var err error
	t.time, err = time.Parse(YmdHisSlash, str)
	return err
}

time テスト

func TestJSONableTargetMonthTime(t *testing.T) {
	type TestTargetMonthSt struct {
		Title string                        `json:"Title"`
		Tm    *util.JSONableTargetMonthTime `json:"Tm"`
	}

	st := TestTargetMonthSt{
		"ほげ",
		util.NewJSONableTargetMonthTime("201103"),
	}
	assert.Equal(t, "2011-03-01T00:00:00Z", st.Tm.Time().Format(time.RFC3339))

	m, err := json.Marshal(st)
	assert.Nil(t, err)
	assert.Equal(t, `{"Title":"ほげ","Tm":"201103"}`, string(m))

	actual := TestTargetMonthSt{}
	err = json.Unmarshal(m, &actual)
	assert.Nil(t, err)
	assert.Equal(t, st, actual)
}

func TestJSONableYmdHisTime(t *testing.T) {
	type TestTargetMonthSt struct {
		Title string                   `json:"Title"`
		Tm    *util.JSONableYmdHisTime `json:"Tm"`
	}

	st := TestTargetMonthSt{
		"ほげ",
		util.NewJSONableYmdHisTime("2011/03/23 11:22:33"),
	}
	assert.Equal(t, "2011-03-23T11:22:33Z", st.Tm.Time().Format(time.RFC3339))

	m, err := json.Marshal(st)
	assert.Nil(t, err)
	assert.Equal(t, `{"Title":"ほげ","Tm":"2011/03/23 11:22:33"}`, string(m))

	actual := TestTargetMonthSt{}
	err = json.Unmarshal(m, &actual)
	assert.Nil(t, err)
	assert.Equal(t, st, actual)
}