-
Notifications
You must be signed in to change notification settings - Fork 6
/
file.go
185 lines (178 loc) · 4.64 KB
/
file.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
174
175
176
177
178
179
180
181
182
183
184
185
package main
import (
"encoding/csv"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
)
const (
enedisCustomCSVSep = ';'
headerFields = 9
headerDateFormat = "02/01/2006" // 31/12/2021
dataDateFormat = "2006-01-02T15:04:05-07:00" // 2021-12-31T00:30:00+01:00
noSteppingValue = -1
dataStartLine = 4
defaultStepping = 30 * time.Minute
)
type CSVHeader struct {
PRMID string
Start time.Time
End time.Time
Step time.Duration
}
type point struct {
Time time.Time
Value float64 // kW
Conso float64 // kWh
}
func parseFile(path string) (header CSVHeader, data []point, err error) {
// Prepare the CSV reader
fd, err := os.Open(path)
if err != nil {
err = fmt.Errorf("failed to open file: %w", err)
return
}
defer fd.Close()
cr := csv.NewReader(fd)
cr.Comma = enedisCustomCSVSep
cr.ReuseRecord = true
// Parse the header
if header, err = parseHeader(cr); err != nil {
err = fmt.Errorf("failed to parse header: %w", err)
return
}
// Parse data
if data, err = parseData(cr, header.Step); err != nil {
err = fmt.Errorf("failed to parse data: %w", err)
return
}
return
}
func parseHeader(cr *csv.Reader) (header CSVHeader, err error) {
var records []string
// line 1
records, err = cr.Read()
if err != nil {
err = fmt.Errorf("failed to read first line: %w", err)
return
}
if len(records) != headerFields {
err = fmt.Errorf("invalid headers, expecting %d got %d: %s",
headerFields, len(records), strings.Join(records, ", "))
return
}
// line 2
records, err = cr.Read()
if err != nil {
err = fmt.Errorf("failed to read second line: %w", err)
return
}
header.PRMID = records[0]
header.Start, err = time.ParseInLocation(headerDateFormat, records[2], frLocation)
if err != nil {
err = fmt.Errorf("failed to parse start date from second line: %w", err)
return
}
header.End, err = time.ParseInLocation(headerDateFormat, records[3], frLocation)
if err != nil {
err = fmt.Errorf("failed to parse end date from second line: %w", err)
return
}
step_str := records[8]
if step_str != "" {
var step int
if step, err = strconv.Atoi(step_str); err != nil {
err = fmt.Errorf("non integer stepping found: %s", err)
return
}
header.Step = time.Duration(step) * time.Minute
} else {
// step empty
header.Step = noSteppingValue
}
return
}
func parseData(cr *csv.Reader, stepping time.Duration) (data []point, err error) {
// nb of records changes for data
cr.FieldsPerRecord = 2
// remove data header
_, err = cr.Read()
if err != nil {
err = fmt.Errorf("failed to read the third line: %w", err)
return
}
// Process data lines
var (
records []string
line int
recordTime time.Time
recordValue int
computedkWh float64
)
if stepping == noSteppingValue {
data = make([]point, 0, 365*24*(time.Hour/defaultStepping))
} else {
data = make([]point, 0, 365*24*(time.Hour/stepping))
}
for line = dataStartLine; ; line++ {
// read line
records, err = cr.Read()
if err != nil {
err = fmt.Errorf("failed to parse line: %w", err)
break
}
// skip recorp if empty
if records[1] == "" {
continue
}
// parse line
if recordTime, err = time.Parse(dataDateFormat, records[0]); err != nil {
err = fmt.Errorf("failed to parse record date time: %w", err)
break
}
if recordValue, err = strconv.Atoi(records[1]); err != nil {
err = fmt.Errorf("failed to parse record value: %w", err)
break
}
// check
if recordTime.Second() != 0 {
err = fmt.Errorf("seconds should always be 00: %v", recordTime)
break
}
// Determine stepping if needed
if stepping == noSteppingValue {
if line > dataStartLine {
// get previous point and compute stepping
prevPoint := data[len(data)-1]
stepping := recordTime.Sub(prevPoint.Time)
computedkWh = float64(recordValue) / 1000 / float64(time.Hour/stepping)
if line == dataStartLine+1 {
// compute the first point kWh using the same stepping
firstPoint := data[len(data)-1]
firstPoint.Conso = firstPoint.Value / 1000 / float64(time.Hour/stepping)
data[len(data)-1] = firstPoint
}
}
// else first point, can not compute stepping without a previous point, waiting for second point to retro compute first point
} else {
computedkWh = float64(recordValue) / 1000 / float64(time.Hour/stepping)
}
// save value
data = append(data, point{
Time: recordTime.In(frLocation), // make sure every date time in this program is in the same loc
Value: float64(recordValue) / 1000, // convert W to kW
Conso: computedkWh,
})
}
if errors.Is(err, io.EOF) {
err = nil
}
if err != nil {
err = fmt.Errorf("error at line %d: %w", line, err)
}
return
}