struct → csv は reflect ゴリゴリ
csv → struct は json を経由してサクッと
// StructToCSV struct -> string csv
func StructToCSV(st interface{}) (string, error) {
t := reflect.TypeOf(st)
if t.Kind() != reflect.Struct {
return "", fmt.Errorf("util: StructToCSV failed. %t is not struct", st)
}
v := reflect.ValueOf(st)
results := make([]string, v.NumField())
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
result := fmt.Sprint(f.Interface())
tm, ok := f.Interface().(JSONableTime)
if ok {
result = tm.Format()
}
results[i] = result
}
return strings.Join(results, ","), nil
}
// SliceToCSV []struct -> string csv
func SliceToCSV(sl interface{}) (string, error) {
t := reflect.TypeOf(sl)
if t.Kind() != reflect.Slice || t.Elem().Kind() != reflect.Struct {
return "", fmt.Errorf("util: SliceToCSV failed. %t is not struct slice", sl)
}
v := reflect.ValueOf(sl)
// 要素数が 0 でも panic にならないよう slice の要素型を基準にヘッダーを作る
eT := v.Type().Elem()
results := make([]string, v.Len()+1)
header := make([]string, eT.NumField())
for i := 0; i < eT.NumField(); i++ {
header[i] = eT.Field(i).Tag.Get("label")
}
results[0] = strings.Join(header, ",")
if v.Len() == 0 {
// 要素がからの場合は, ヘッダーのみの csv として返す
return results[0], nil
}
for i := 0; i < v.Len(); i++ {
st := v.Index(i).Interface()
csv, err := StructToCSV(st)
if err != nil {
return "", err
}
results[i+1] = csv
}
return strings.Join(results, "\\n"), nil
}
// CSVToSlice csv -> []struct
func CSVToSlice(csv string, sl interface{}) error {
s := strings.Split(csv, "\\n")
header := strings.Split(s[0], ",")
headerCellMapSlice := make([]map[string]string, len(s[1:]))
for i, row := range s[1:] {
headerCellMap := map[string]string{}
for i, v := range strings.Split(row, ",") {
headerCellMap[header[i]] = v
}
headerCellMapSlice[i] = headerCellMap
}
m, err := json.Marshal(headerCellMapSlice)
if err != nil {
return err
}
if err := json.Unmarshal(m, sl); err != nil {
return err
}
return nil
}
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)
})
}
}
// 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
}
json.Marshal
↔ json.Unmarshal
した時, 期待通りであること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)
}