-
-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathcsv_data.go
More file actions
324 lines (286 loc) · 7.21 KB
/
csv_data.go
File metadata and controls
324 lines (286 loc) · 7.21 KB
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package main
import (
"bytes"
"encoding/csv"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
)
// CSVFile represents a single CSV file with its headers and data
type CSVFile struct {
Name string
Path string
Headers []string
Rows [][]string
UseCRLF bool
}
// CSVFileCollection represents all CSV files loaded from a directory
type CSVFileCollection struct {
Files []*CSVFile
}
// loadCSVRaw loads a CSV file without struct mapping, preserving all columns
func loadCSVRaw(filePath string) (*CSVFile, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("error opening file %s: %v", filePath, err)
}
defer file.Close()
// Detect CRLF line endings by reading the first chunk
useCRLF := false
buf := make([]byte, 4096)
n, _ := file.Read(buf)
if n > 0 {
useCRLF = bytes.Contains(buf[:n], []byte("\r\n"))
}
if _, err := file.Seek(0, 0); err != nil {
return nil, fmt.Errorf("error seeking file %s: %v", filePath, err)
}
reader := csv.NewReader(file)
reader.Comma = ','
reader.LazyQuotes = true
reader.FieldsPerRecord = -1 // Allow variable number of fields per record
// Read headers
headers, err := reader.Read()
if err != nil {
return nil, fmt.Errorf("error reading headers from %s: %v", filePath, err)
}
// Trim whitespace from headers
for i := range headers {
headers[i] = strings.TrimSpace(headers[i])
}
// Read all rows
var rows [][]string
lineNum := 1 // Start at 1 since headers are line 0
for {
row, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
// Skip malformed rows and continue
fmt.Printf("Warning: error reading row %d from %s: %v\n", lineNum, filePath, err)
lineNum++
continue
}
rows = append(rows, row)
lineNum++
}
return &CSVFile{
Name: filepath.Base(filePath),
Path: filePath,
Headers: headers,
Rows: rows,
UseCRLF: useCRLF,
}, nil
}
// loadAllCSVFiles loads all CSV files from a directory
func loadAllCSVFiles(dir string) (*CSVFileCollection, error) {
collection := &CSVFileCollection{
Files: []*CSVFile{},
}
files, err := filepath.Glob(filepath.Join(dir, "*.csv"))
if err != nil {
return nil, fmt.Errorf("error finding CSV files in directory %s: %v", dir, err)
}
for _, filePath := range files {
csvFile, err := loadCSVRaw(filePath)
if err != nil {
// Log error but continue loading other files
fmt.Printf("Warning: error loading CSV file %s: %v\n", filePath, err)
continue
}
collection.Files = append(collection.Files, csvFile)
}
if len(collection.Files) == 0 {
return nil, fmt.Errorf("no valid CSV files found in directory %s", dir)
}
return collection, nil
}
// GetCombinedPartmaster returns all parts from all CSV files as a partmaster
func (c *CSVFileCollection) GetCombinedPartmaster() (partmaster, error) {
pm := partmaster{}
for _, file := range c.Files {
// Try to parse each file as partmaster format
filePM, err := c.parseFileAsPartmaster(file)
if err != nil {
// Skip files that don't match partmaster format
continue
}
pm = append(pm, filePM...)
}
return pm, nil
}
// parseFileAsPartmaster attempts to parse a CSV file as partmaster format
func (c *CSVFileCollection) parseFileAsPartmaster(file *CSVFile) (partmaster, error) {
pm := partmaster{}
// Find column indices for partmaster fields
ipnIdx := -1
descIdx := -1
footprintIdx := -1
valueIdx := -1
mfrIdx := -1
mpnIdx := -1
datasheetIdx := -1
priorityIdx := -1
checkedIdx := -1
for i, header := range file.Headers {
switch header {
case "IPN":
ipnIdx = i
case "Description":
descIdx = i
case "Footprint":
footprintIdx = i
case "Value":
valueIdx = i
case "Manufacturer":
mfrIdx = i
case "MPN":
mpnIdx = i
case "Datasheet":
datasheetIdx = i
case "Priority":
priorityIdx = i
case "Checked":
checkedIdx = i
}
}
// Must have at least IPN column to be valid
if ipnIdx == -1 {
return nil, fmt.Errorf("no IPN column found")
}
// Parse rows
for _, row := range file.Rows {
if len(row) == 0 || len(row) <= ipnIdx {
continue
}
line := &partmasterLine{}
// Parse IPN
ipnVal, err := newIpn(row[ipnIdx])
if err != nil {
continue // Skip invalid IPNs
}
line.IPN = ipnVal
// Parse other fields if they exist
if descIdx >= 0 && len(row) > descIdx {
line.Description = row[descIdx]
}
if footprintIdx >= 0 && len(row) > footprintIdx {
line.Footprint = row[footprintIdx]
}
if valueIdx >= 0 && len(row) > valueIdx {
line.Value = row[valueIdx]
}
if mfrIdx >= 0 && len(row) > mfrIdx {
line.Manufacturer = row[mfrIdx]
}
if mpnIdx >= 0 && len(row) > mpnIdx {
line.MPN = row[mpnIdx]
}
if datasheetIdx >= 0 && len(row) > datasheetIdx {
line.Datasheet = row[datasheetIdx]
}
if priorityIdx >= 0 && len(row) > priorityIdx {
// Parse priority as int, default to 0 if invalid
var priority int
fmt.Sscanf(row[priorityIdx], "%d", &priority)
line.Priority = priority
}
if checkedIdx >= 0 && len(row) > checkedIdx {
line.Checked = row[checkedIdx]
}
pm = append(pm, line)
}
return pm, nil
}
// saveCSVRaw writes headers and rows back to the CSV file on disk.
func saveCSVRaw(csvFile *CSVFile) error {
f, err := os.Create(csvFile.Path)
if err != nil {
return fmt.Errorf("error creating file %s: %v", csvFile.Path, err)
}
defer f.Close()
w := csv.NewWriter(f)
w.UseCRLF = csvFile.UseCRLF
if err := w.Write(csvFile.Headers); err != nil {
return fmt.Errorf("error writing headers: %v", err)
}
for _, row := range csvFile.Rows {
if err := w.Write(row); err != nil {
return fmt.Errorf("error writing row: %v", err)
}
}
w.Flush()
return w.Error()
}
// findHeaderIndex returns the index of name in headers, or -1 if not found.
func findHeaderIndex(headers []string, name string) int {
for i, h := range headers {
if h == name {
return i
}
}
return -1
}
// sortRowsByIPN sorts rows in-place by the IPN column value.
func sortRowsByIPN(rows [][]string, ipnColIdx int) {
if ipnColIdx < 0 {
return
}
sort.SliceStable(rows, func(i, j int) bool {
a, b := "", ""
if ipnColIdx < len(rows[i]) {
a = rows[i][ipnColIdx]
}
if ipnColIdx < len(rows[j]) {
b = rows[j][ipnColIdx]
}
return a < b
})
}
// nextAvailableIPN scans rows to determine the category (CCC) and the maximum
// NNN value, then returns CCC-(NNN+1)-0001 as the next available IPN string.
func nextAvailableIPN(rows [][]string, ipnColIdx int) (string, error) {
if ipnColIdx < 0 {
return "", fmt.Errorf("no IPN column")
}
category := ""
maxN := 0
nDigits := 3
for _, row := range rows {
if ipnColIdx >= len(row) {
continue
}
parsed, err := newIpn(row[ipnColIdx])
if err != nil {
continue
}
c, _ := parsed.c()
n, _ := parsed.n()
if category == "" {
category = c
nDigits = parsed.nWidth()
}
if n > maxN {
maxN = n
}
}
if category == "" {
return "", fmt.Errorf("no valid IPNs found to determine category")
}
newN := maxN + 1
// If existing IPNs use 4-digit N, preserve that format
if nDigits >= 4 || newN > 999 {
nDigits = 4
}
nFmt := fmt.Sprintf("%%0%dv", nDigits)
newIPNStr := fmt.Sprintf("%v-"+nFmt+"-%04v", category, newN, 1)
newIPN, err := newIpn(newIPNStr)
if err != nil {
return "", fmt.Errorf("error creating new IPN: %v", err)
}
return string(newIPN), nil
}