@@ -14,18 +14,29 @@ import (
1414)
1515
1616const (
17+ // Supported Road Trip vehicle data file version "1500,en" only.
18+ SupportedVersion int = 1500
19+
1720 // Remove erroneous header fields for VEHICLE section
1821 // per Darren Stone 2024-12-09 via email.
1922 RemoveErroneousHeaders = true
2023)
2124
25+ // RawFileData contains the raw contents read from a single Road Trip data
26+ // file.
27+ type RawFileData []byte
28+
29+ // RawSectionData contains the raw contents read from a single section of a
30+ // single Road Trip data file.
31+ type RawSectionData []byte
32+
2233// VehicleOptions contain the options to be used when creating a new Vehicle object.
2334type VehicleOptions struct {
2435 Logger * slog.Logger
2536 LogLevel slog.Level
2637}
2738
28- // A Vehicle holds the parsed sections contained in a Road Trip CSV backup file.
39+ // A Vehicle holds the parsed sections contained in a Road Trip vehicle data file.
2940type Vehicle struct {
3041 Delimiters string
3142 Version int
@@ -37,37 +48,49 @@ type Vehicle struct {
3748 Trips []TripRecord `roadtrip:"ROAD TRIPS"`
3849 Tires []TireRecord `roadtrip:"TIRE LOG"`
3950 Valuations []ValuationRecord `roadtrip:"VALUATIONS"`
40- Raw [] byte
51+ Raw RawFileData
4152 logger * slog.Logger
4253 logLevel slog.Level
4354}
4455
45- func UnmarshalRoadtripSection (data []byte , target any ) error {
46- header , err := SectionHeader (target )
47- if err != nil {
48- return err
56+ // NewVehicle returns a new, empty [Vehicle] object.
57+ func NewVehicle (options VehicleOptions ) Vehicle {
58+ var v Vehicle
59+
60+ if options .Logger == nil {
61+ options .Logger = slog .New (slog .NewTextHandler (nil , nil ))
4962 }
5063
51- sectionData := GetSectionContents (data , header )
64+ v .logger = options .Logger
65+ v .logLevel = options .LogLevel
5266
53- _ , err = cvslib .Unmarshal (sectionData , target )
67+ return v
68+ }
69+
70+ // NewVehicleFromFile returns a new [Vehicle] object populated with data read
71+ // and parsed from the file.
72+ func NewVehicleFromFile (filename string , options VehicleOptions ) (Vehicle , error ) {
73+ v := NewVehicle (options )
74+
75+ err := v .LoadFile (filename )
5476 if err != nil {
55- return err
77+ return v , err
5678 }
5779
58- return nil
80+ return v , nil
5981}
6082
6183// Each Road Trip "CSV" file is actually multiple, independent blocks of CSV
6284// data delimited by two newlines and a section header string in all capital
6385// letters.
6486//
6587// SectionHeaderList returns a slice of strings corresponding to each of the
66- // section headers found in the Road Trip data file. Currently this package
67- // only supports Language "en" (see known issues in the README.md file).
88+ // section headers expected in the Road Trip vehicle data file. Currently this
89+ // package only supports Language "en" (see known issues in the README.md
90+ // file).
6891//
69- // This list is built by inspecting the `roadtrip` struct tags present in
70- // the [Vehicle] struct definition.
92+ // This list is built by inspecting the `roadtrip` struct tags present in the
93+ // [Vehicle] struct definition.
7194func SectionHeaderList () []string {
7295 var headerList []string
7396
@@ -83,10 +106,10 @@ func SectionHeaderList() []string {
83106 return headerList
84107}
85108
86- // SectionHeader will return the section header for any suitable target field
87- // in the [Vehicle] struct. It's used to identify the correct CSV block in the
88- // Road Trip CSV file.
89- func SectionHeader (target any ) (string , error ) {
109+ // SectionHeaderForTarget will return the section header for any suitable
110+ // target field in the [Vehicle] struct. It's used to identify the correct CSV
111+ // block in the Road Trip vehicle data file.
112+ func SectionHeaderForTarget (target any ) (string , error ) {
90113 targetType := reflect .TypeOf (target ).Elem ()
91114
92115 vt := reflect .TypeOf (Vehicle {})
@@ -103,36 +126,59 @@ func SectionHeader(target any) (string, error) {
103126 return "" , fmt .Errorf ("cannot unmarshal %s, missing roadtrip struct tag" , targetType )
104127}
105128
106- // New returns a new, empty [Vehicle] with a no-op logger.
107- func NewVehicle (options VehicleOptions ) Vehicle {
108- var v Vehicle
129+ // GetSectionContents evaluates the raw content from a Road Trip data file and extracts only
130+ // the single section block identified by the supplied section header string value.
131+ func (fileData * RawFileData ) GetSectionContents (sectionHeader string ) RawSectionData {
132+ sectionStart := make (map [string ]int )
109133
110- if options .Logger == nil {
111- options .Logger = slog .New (slog .NewTextHandler (nil , nil ))
134+ dataBytes := reflect .ValueOf (* fileData ).Bytes ()
135+
136+ for _ , element := range SectionHeaderList () {
137+ i := bytes .Index (dataBytes , []byte (element ))
138+ sectionStart [element ] = i
112139 }
113140
114- v . logger = options . Logger
115- v . logLevel = options . LogLevel
141+ startPosition := sectionStart [ sectionHeader ]
142+ endPosition := len ( dataBytes )
116143
117- return v
144+ for _ , e := range sectionStart {
145+ if e > startPosition && e < endPosition {
146+ endPosition = e - 1
147+ }
148+ }
149+
150+ // Don't include the section header line in the outbuf
151+ startPosition = startPosition + len (sectionHeader ) + 1
152+
153+ outbuf := dataBytes [startPosition :endPosition ]
154+
155+ return outbuf
118156}
119157
120- // NewFromFile returns a new [Vehicle] populated with data read and parsed
121- // from the file.
122- func NewVehicleFromFile (filename string , options VehicleOptions ) (Vehicle , error ) {
123- v := NewVehicle (options )
158+ // UnmarshalRoadtripSection takes the raw contents of a Road Trip vehicle data
159+ // file, extracts only the relevant section block, and then parses it into
160+ // appropriate struct field based on the type of the target variable.
161+ //
162+ // This relies on an accurate struct tag on the [Vehicle] field in question
163+ // which instructs the function on which section header line to look for.
164+ func (fileData * RawFileData ) UnmarshalRoadtripSection (target any ) error {
165+ header , err := SectionHeaderForTarget (target )
166+ if err != nil {
167+ return err
168+ }
124169
125- err := v .LoadFile (filename )
170+ sectionData := fileData .GetSectionContents (header )
171+
172+ _ , err = cvslib .Unmarshal (sectionData , target )
126173 if err != nil {
127- return v , err
174+ return err
128175 }
129176
130- return v , nil
177+ return nil
131178}
132179
133- // SetLogger optionally binds an [slog.Logger] to a [Vehicle] for internal
134- // package debugging. If you do not call SetLogger, log output will be
135- // discarded during package operation.
180+ // SetLogger optionally sets the [Vehicle] logger for internal package
181+ // debugging.
136182func (v * Vehicle ) SetLogger (l * slog.Logger ) {
137183 v .logger = l
138184 v .logLevel = slog .LevelInfo
@@ -145,7 +191,7 @@ func (v *Vehicle) SetLogLoggerLevel(levelInfo slog.Level) slog.Level {
145191 return slog .SetLogLoggerLevel (levelInfo )
146192}
147193
148- // LogValue is the handler for [log.slog] to emit structured output for a
194+ // LogValue is the handler for [log.slog] to emit structured output for the
149195// [Vehicle] object when logging.
150196func (v * Vehicle ) LogValue () slog.Value {
151197 var value slog.Value
@@ -167,8 +213,10 @@ func (v *Vehicle) LogValue() slog.Value {
167213 return value
168214}
169215
170- // LoadFile reads and parses a file into a [Vehicle] variable .
216+ // LoadFile reads and parses a file into the [Vehicle] object .
171217func (v * Vehicle ) LoadFile (filename string ) error {
218+ var buf RawFileData
219+
172220 buf , err := os .ReadFile (filename )
173221 if err != nil {
174222 return err
@@ -184,11 +232,16 @@ func (v *Vehicle) LoadFile(filename string) error {
184232 return v .UnmarshalRoadtrip (buf )
185233}
186234
187- func (v * Vehicle ) UnmarshalRoadtrip (data []byte ) error {
235+ // UnmarshalRoadtrip takes the raw contents of a Road Trip data file and
236+ // and populates the [Vehicle] object with what it finds inside.
237+ func (v * Vehicle ) UnmarshalRoadtrip (data RawFileData ) error {
188238 v .Raw = data
189239
190240 var err error
191241
242+ // This seems ripe for future improvement, it should be possible
243+ // to generate the targets array by reflecting through v and finding
244+ // the correct pointers to append.
192245 var targets []any
193246 targets = append (targets , & v .Vehicles )
194247 targets = append (targets , & v .FuelRecords )
@@ -198,13 +251,13 @@ func (v *Vehicle) UnmarshalRoadtrip(data []byte) error {
198251 targets = append (targets , & v .Valuations )
199252
200253 for _ , target := range targets {
201- err = UnmarshalRoadtripSection (data , target )
254+ err = data . UnmarshalRoadtripSection (target )
202255 if err != nil {
203256 return fmt .Errorf ("unable to parse %s: %w" , target , err )
204257 }
205258 }
206259
207- v .logger .Info ("Loaded Road Trip CSV " ,
260+ v .logger .Info ("Loaded Road Trip vehicle data file " ,
208261 "filename" , v .Filename ,
209262 "bytes" , len (data ),
210263 "vehicleRecords" , len (v .Vehicles ),
@@ -218,33 +271,6 @@ func (v *Vehicle) UnmarshalRoadtrip(data []byte) error {
218271 return nil
219272}
220273
221- // Section returns a byte slice containing the raw contents of the specified section
222- // from the corresponding [Vehicle.Raw] object.
223- func GetSectionContents (data []byte , sectionHeader string ) []byte {
224- sectionStart := make (map [string ]int )
225-
226- for _ , element := range SectionHeaderList () {
227- i := bytes .Index (data , []byte (element ))
228- sectionStart [element ] = i
229- }
230-
231- startPosition := sectionStart [sectionHeader ]
232- endPosition := len (data )
233-
234- for _ , e := range sectionStart {
235- if e > startPosition && e < endPosition {
236- endPosition = e - 1
237- }
238- }
239-
240- // Don't include the section header line in the outbuf
241- startPosition = startPosition + len (sectionHeader ) + 1
242-
243- outbuf := data [startPosition :endPosition ]
244-
245- return outbuf
246- }
247-
248274// ParseDate parses a Road Trip styled date string and turns it into a proper
249275// Go [time.Time] value.
250276func ParseDate (dateString string ) (time.Time , error ) {
0 commit comments