Skip to content

Commit

Permalink
feat: add AlignRecord flag to Decoder
Browse files Browse the repository at this point in the history
Closes: #63
  • Loading branch information
jszwec committed Sep 10, 2023
1 parent 5633865 commit 4c3b8e6
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 1 deletion.
25 changes: 24 additions & 1 deletion decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4c3b8e6

Please sign in to comment.