-
Notifications
You must be signed in to change notification settings - Fork 2
/
reader.go
173 lines (152 loc) · 3.57 KB
/
reader.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
// Package csvcolumn reads specified columns from a csv style input.
package csvcolumn
import (
"encoding/csv"
"io"
"strings"
)
type field struct {
Name string
Column int
Value Value
}
const (
// ColumnUnbound is an init value of a field.Column
ColumnUnbound = -2
// ColumnMissing is an field.Column value used when field.Name was not
// found in the headers of the source
ColumnMissing = -1
)
// NewReader is a factory function to create a *Reader
func NewReader(src io.Reader) *Reader {
return &Reader{
Comma: ',',
r: src,
}
}
// Reader keeps the configuration and state of reading from source
type Reader struct {
Comma rune
Comment rune
FieldsPerRecord int
LazyQuotes bool
TrimLeadingSpace bool
CaseSensitiveHeader bool
r io.Reader
err error
parser *csv.Reader
headers []string
fields []field
}
func (r *Reader) init() {
r.parser = csv.NewReader(r.r)
r.parser.Comma = r.Comma
r.parser.Comment = r.Comment
r.parser.FieldsPerRecord = r.FieldsPerRecord
r.parser.LazyQuotes = r.LazyQuotes
r.parser.TrimLeadingSpace = r.TrimLeadingSpace
r.parser.ReuseRecord = true
r.bind()
}
// bind initializes the Reader reading state.
// It parses the headers from source to validate columns of interest
// are present in a header.
func (r *Reader) bind() {
if len(r.headers) == 0 {
r.headers, r.err = r.parser.Read()
if r.err != nil {
return
}
}
for i := range r.fields {
field := &r.fields[i]
if field.Column != ColumnUnbound {
continue
}
if r.CaseSensitiveHeader {
for k, header := range r.headers {
if field.Name == header {
field.Column = k
break
}
}
} else {
for k, header := range r.headers {
if strings.EqualFold(field.Name, header) {
field.Column = k
break
}
}
}
if field.Column == ColumnUnbound {
field.Column = ColumnMissing
}
}
}
// Next parses a next line from a source.
func (r *Reader) Next() (ok bool) {
if r.parser == nil {
r.init()
if r.err != nil {
return false
}
}
record, err := r.parser.Read()
if err == io.EOF {
return false
}
if err != nil && r.err == nil {
r.err = err
return false
}
for i := range r.fields {
field := &r.fields[i]
if field.Column < 0 {
continue
}
cell := record[field.Column]
err := field.Value.Scan(cell)
if err != nil && r.err == nil {
r.err = err
return false
}
}
return true
}
// Err returns the latest error of a reader.
func (r *Reader) Err() error { return r.err }
// Bind binds a column in a csv to its matching Value struct.
// It's useful when you are interested in adding your own Value types.
func (r *Reader) Bind(columnName string, value Value) {
if r.parser != nil {
panic("binding must be done before calling Next")
}
r.fields = append(r.fields,
field{
Name: columnName,
Column: ColumnUnbound,
Value: value,
},
)
}
// String returns a pointer to a string field value of a given columnName
// which is reassigned with every Next() call.
func (r *Reader) String(columnName string) *string {
value := &String{}
r.Bind(columnName, value)
return &value.Value
}
// Int returns a pointer to an int field value of a given columnName which
// is reassigned with every Next() call.
func (r *Reader) Int(columnName string) *int {
value := &Int{}
r.Bind(columnName, value)
return &value.Value
}
// Float64 returns a pointer to an float64 field value of a given columnName which
// is reassigned with every Next() call.
func (r *Reader) Float64(columnName string) *float64 {
value := &Float64{}
r.Bind(columnName, value)
return &value.Value
}