diff --git a/decoder.go b/decoder.go index 97daf2f..90cf6cb 100644 --- a/decoder.go +++ b/decoder.go @@ -25,6 +25,21 @@ type Decoder struct { // provided struct. DisallowMissingColumns bool + // AlignRecord will cause Decoder to align returned record slice to the + // header in case Reader returns records of different lengths. + // + // This flag is supposed to work with csv.Reader.FieldsPerRecord set to -1 + // which may cause this behavior. + // + // When header is longer than the record, it will populate the missing + // records with an empty string. + // + // When header is shorter than the record, it will slice the record to match + // header's length. + // + // When this flag is used, Decoder will not ever return ErrFieldCount. + AlignRecord bool + // If not nil, Map is a function that is called for each field in the csv // record before decoding the data. It allows mapping certain string values // for specific columns or types to a known format. Decoder calls Map with @@ -394,7 +409,15 @@ func (d *Decoder) decodeStruct(v reflect.Value) (err error) { } if len(d.record) != len(d.header) { - return ErrFieldCount + if !d.AlignRecord { + return ErrFieldCount + } + + if len(d.record) > len(d.header) { + d.record = d.record[:len(d.header)] + } else { + d.record = append(d.record, make([]string, len(d.header)-len(d.record))...) + } } return d.unmarshal(d.record, v) diff --git a/decoder_test.go b/decoder_test.go index 083f522..f0c0765 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -2483,6 +2483,56 @@ s,1,3.14,true t.Fatal("want err not to be nil") } }) + + t.Run("align record to header - header longer", func(t *testing.T) { + csvr := csv.NewReader(strings.NewReader("A,B,C\na,b")) + csvr.FieldsPerRecord = -1 + dec, err := NewDecoder(csvr) + if err != nil { + t.Fatalf("want err == nil; got %v", err) + } + dec.AlignRecord = true + + var data []struct { + A, B, C string + } + if err := dec.Decode(&data); err != nil { + t.Fatal("did not expect decode fail with:", err) + } + + if len(data) != 1 { + t.Fatalf("expected data to be of length 1 got: %d", len(data)) + } + + if data[0].A != "a" || data[0].B != "b" || data[0].C != "" { + t.Errorf("expected \"a\", \"b\" and \"\"; got: %q, %q and %q", data[0].A, data[0].B, data[0].C) + } + }) + + t.Run("align record to header - header shorter", func(t *testing.T) { + csvr := csv.NewReader(strings.NewReader("A,B\na,b,c")) + csvr.FieldsPerRecord = -1 + dec, err := NewDecoder(csvr) + if err != nil { + t.Fatalf("want err == nil; got %v", err) + } + dec.AlignRecord = true + + var data []struct { + A, B string + } + if err := dec.Decode(&data); err != nil { + t.Fatal("did not expect decode fail with:", err) + } + + if len(data) != 1 { + t.Fatalf("expected data to be of length 1 got: %d", len(data)) + } + + if data[0].A != "a" || data[0].B != "b" { + t.Errorf("expected \"a\" and \"b\"; got: %q and %q", data[0].A, data[0].B) + } + }) } func BenchmarkDecode(b *testing.B) { diff --git a/error.go b/error.go index 15da059..6c6f800 100644 --- a/error.go +++ b/error.go @@ -10,6 +10,8 @@ import ( // ErrFieldCount is returned when header's length doesn't match the length of // the read record. +// +// This Error can be disabled with Decoder.AlignRecord = true. var ErrFieldCount = errors.New("wrong number of fields in record") // An UnmarshalTypeError describes a string value that was not appropriate for