1+ package main
2+
3+ import (
4+ "encoding/csv"
5+ "fmt"
6+ "io"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ )
11+
12+ // CSVFile represents a single CSV file with its headers and data
13+ type CSVFile struct {
14+ Name string
15+ Path string
16+ Headers []string
17+ Rows [][]string
18+ }
19+
20+ // CSVFileCollection represents all CSV files loaded from a directory
21+ type CSVFileCollection struct {
22+ Files []* CSVFile
23+ }
24+
25+ // loadCSVRaw loads a CSV file without struct mapping, preserving all columns
26+ func loadCSVRaw (filePath string ) (* CSVFile , error ) {
27+ file , err := os .Open (filePath )
28+ if err != nil {
29+ return nil , fmt .Errorf ("error opening file %s: %v" , filePath , err )
30+ }
31+ defer file .Close ()
32+
33+ reader := csv .NewReader (file )
34+ reader .Comma = ','
35+ reader .LazyQuotes = true
36+ reader .FieldsPerRecord = - 1 // Allow variable number of fields per record
37+
38+ // Read headers
39+ headers , err := reader .Read ()
40+ if err != nil {
41+ return nil , fmt .Errorf ("error reading headers from %s: %v" , filePath , err )
42+ }
43+
44+ // Trim whitespace from headers
45+ for i := range headers {
46+ headers [i ] = strings .TrimSpace (headers [i ])
47+ }
48+
49+ // Read all rows
50+ var rows [][]string
51+ lineNum := 1 // Start at 1 since headers are line 0
52+ for {
53+ row , err := reader .Read ()
54+ if err == io .EOF {
55+ break
56+ }
57+ if err != nil {
58+ // Skip malformed rows and continue
59+ fmt .Printf ("Warning: error reading row %d from %s: %v\n " , lineNum , filePath , err )
60+ lineNum ++
61+ continue
62+ }
63+ rows = append (rows , row )
64+ lineNum ++
65+ }
66+
67+ return & CSVFile {
68+ Name : filepath .Base (filePath ),
69+ Path : filePath ,
70+ Headers : headers ,
71+ Rows : rows ,
72+ }, nil
73+ }
74+
75+ // loadAllCSVFiles loads all CSV files from a directory
76+ func loadAllCSVFiles (dir string ) (* CSVFileCollection , error ) {
77+ collection := & CSVFileCollection {
78+ Files : []* CSVFile {},
79+ }
80+
81+ files , err := filepath .Glob (filepath .Join (dir , "*.csv" ))
82+ if err != nil {
83+ return nil , fmt .Errorf ("error finding CSV files in directory %s: %v" , dir , err )
84+ }
85+
86+ for _ , filePath := range files {
87+ csvFile , err := loadCSVRaw (filePath )
88+ if err != nil {
89+ // Log error but continue loading other files
90+ fmt .Printf ("Warning: error loading CSV file %s: %v\n " , filePath , err )
91+ continue
92+ }
93+ collection .Files = append (collection .Files , csvFile )
94+ }
95+
96+ if len (collection .Files ) == 0 {
97+ return nil , fmt .Errorf ("no valid CSV files found in directory %s" , dir )
98+ }
99+
100+ return collection , nil
101+ }
102+
103+ // GetCombinedPartmaster returns all parts from all CSV files as a partmaster
104+ func (c * CSVFileCollection ) GetCombinedPartmaster () (partmaster , error ) {
105+ pm := partmaster {}
106+
107+ for _ , file := range c .Files {
108+ // Try to parse each file as partmaster format
109+ filePM , err := c .parseFileAsPartmaster (file )
110+ if err != nil {
111+ // Skip files that don't match partmaster format
112+ continue
113+ }
114+ pm = append (pm , filePM ... )
115+ }
116+
117+ return pm , nil
118+ }
119+
120+ // parseFileAsPartmaster attempts to parse a CSV file as partmaster format
121+ func (c * CSVFileCollection ) parseFileAsPartmaster (file * CSVFile ) (partmaster , error ) {
122+ pm := partmaster {}
123+
124+ // Find column indices for partmaster fields
125+ ipnIdx := - 1
126+ descIdx := - 1
127+ footprintIdx := - 1
128+ valueIdx := - 1
129+ mfrIdx := - 1
130+ mpnIdx := - 1
131+ datasheetIdx := - 1
132+ priorityIdx := - 1
133+ checkedIdx := - 1
134+
135+ for i , header := range file .Headers {
136+ switch header {
137+ case "IPN" :
138+ ipnIdx = i
139+ case "Description" :
140+ descIdx = i
141+ case "Footprint" :
142+ footprintIdx = i
143+ case "Value" :
144+ valueIdx = i
145+ case "Manufacturer" :
146+ mfrIdx = i
147+ case "MPN" :
148+ mpnIdx = i
149+ case "Datasheet" :
150+ datasheetIdx = i
151+ case "Priority" :
152+ priorityIdx = i
153+ case "Checked" :
154+ checkedIdx = i
155+ }
156+ }
157+
158+ // Must have at least IPN column to be valid
159+ if ipnIdx == - 1 {
160+ return nil , fmt .Errorf ("no IPN column found" )
161+ }
162+
163+ // Parse rows
164+ for _ , row := range file .Rows {
165+ if len (row ) == 0 || len (row ) <= ipnIdx {
166+ continue
167+ }
168+
169+ line := & partmasterLine {}
170+
171+ // Parse IPN
172+ ipnVal , err := newIpn (row [ipnIdx ])
173+ if err != nil {
174+ continue // Skip invalid IPNs
175+ }
176+ line .IPN = ipnVal
177+
178+ // Parse other fields if they exist
179+ if descIdx >= 0 && len (row ) > descIdx {
180+ line .Description = row [descIdx ]
181+ }
182+ if footprintIdx >= 0 && len (row ) > footprintIdx {
183+ line .Footprint = row [footprintIdx ]
184+ }
185+ if valueIdx >= 0 && len (row ) > valueIdx {
186+ line .Value = row [valueIdx ]
187+ }
188+ if mfrIdx >= 0 && len (row ) > mfrIdx {
189+ line .Manufacturer = row [mfrIdx ]
190+ }
191+ if mpnIdx >= 0 && len (row ) > mpnIdx {
192+ line .MPN = row [mpnIdx ]
193+ }
194+ if datasheetIdx >= 0 && len (row ) > datasheetIdx {
195+ line .Datasheet = row [datasheetIdx ]
196+ }
197+ if priorityIdx >= 0 && len (row ) > priorityIdx {
198+ // Parse priority as int, default to 0 if invalid
199+ var priority int
200+ fmt .Sscanf (row [priorityIdx ], "%d" , & priority )
201+ line .Priority = priority
202+ }
203+ if checkedIdx >= 0 && len (row ) > checkedIdx {
204+ line .Checked = row [checkedIdx ]
205+ }
206+
207+ pm = append (pm , line )
208+ }
209+
210+ return pm , nil
211+ }
0 commit comments