1#![deny(clippy::all)]
3#![deny(warnings)]
4use serde::de::Error as SerdeDeError;
5use serde::{de, Deserialize, Serialize};
6use std::{collections, convert, error, fmt, io, num}; use chrono::TimeZone;
9use chrono_tz::Australia::Brisbane;
10use log::info;
11
12pub mod daily;
13pub mod dispatch_is;
14pub mod dispatch_scada;
15pub mod predispatch_is;
16pub mod predispatch_sensitivities;
17pub mod rooftop_actual;
18pub mod rooftop_forecast;
19pub mod yestbid;
20
21pub fn to_nem_date(ndt: &chrono::NaiveDateTime) -> chrono::Date<chrono_tz::Tz> {
23 Brisbane.from_local_datetime(ndt).unwrap().date()
24}
25
26#[derive(Debug)]
27pub enum Error {
28 MissingFooterRecord,
30 MissingHeaderRecord,
31 MissingFile(FileKey),
33 EmptyRow,
35 UnexpectedRowType(String),
36 TooShortRow(usize),
37 IncorrectLineCount { got: usize, expected: usize },
38 ThreadBroken,
39 ParseInt(num::ParseIntError),
40 Csv(csv::Error),
41}
42
43impl fmt::Display for Error {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::MissingHeaderRecord => write!(f, "aemo file is missing the first `c` record"),
47 Self::MissingFooterRecord => write!(f, "aemo file is missing the final `c` record"),
48 Self::MissingFile((name, sub_name, version)) => write!(
49 f,
50 "aemo file was missing {}.{}.v{} section in the file ",
51 name, sub_name, version
52 ),
53 Self::EmptyRow => write!(f, "aemo file row is empty"),
54 Self::UnexpectedRowType(t) => write!(f, "unexpeted row type of {}", t),
55 Self::TooShortRow(len) => {
56 write!(f, "aemo file data row of length {} is too short", len)
57 }
58 Self::IncorrectLineCount { got, expected } => write!(
59 f,
60 "aemo file was supposed to be {} lines long but was instead {} lines long",
61 expected, got
62 ),
63 Self::ThreadBroken => write!(f, "Broken Thread"),
64 Self::ParseInt(e) => write!(f, "parse int error: {}", e),
65 Self::Csv(e) => write!(f, "csv error: {}", e),
66 }
67 }
68}
69
70impl From<num::ParseIntError> for Error {
71 fn from(error: num::ParseIntError) -> Self {
72 Error::ParseInt(error)
73 }
74}
75
76impl From<csv::Error> for Error {
77 fn from(error: csv::Error) -> Self {
78 Error::Csv(error)
79 }
80}
81
82impl error::Error for Error {}
83
84type Result<T> = std::result::Result<T, Error>;
85
86#[derive(Deserialize, Serialize, Debug, Clone)]
87pub struct AemoHeader {
88 record_type: char,
89 data_source: String,
90 file_name: String,
91 participant_name: String,
92 privacy_level: String,
93 #[serde(deserialize_with = "au_date_deserialize")]
94 effective_date: chrono::NaiveDate,
95 #[serde(deserialize_with = "au_time_deserialize")]
96 effective_time: chrono::NaiveTime,
97 serial_number: u64,
98 file_name_2: String,
99 serial_number_2: u64,
100}
101
102#[derive(Deserialize, Serialize, Debug, Clone)]
103struct AemoFooter {
104 record_type: char,
105 end_of_report: String,
106 line_count_inclusive: usize,
107}
108
109#[derive(Debug, Clone)]
110pub struct RawAemoFile {
111 pub header: AemoHeader,
112 pub data: collections::HashMap<FileKey, Vec<csv::StringRecord>>,
113 }
115
116pub type FileKey = (String, String, i32);
117
118impl RawAemoFile {
121 pub fn from_bufread(br: impl io::Read) -> Result<Self> {
122 let mut reader = csv::ReaderBuilder::new()
123 .has_headers(false)
124 .flexible(true)
125 .from_reader(br);
126 let mut records = reader.records();
127 let header: AemoHeader = records
128 .next()
129 .ok_or(Error::MissingHeaderRecord)??
130 .deserialize(None)?;
131
132 let mut footer: Result<AemoFooter> = Err(Error::MissingFooterRecord);
134 let mut data: collections::HashMap<FileKey, Vec<csv::StringRecord>> =
135 collections::HashMap::new();
136
137 for record in records {
138 let record = record?;
139 match record.get(0) {
140 Some("C") => {
141 footer = record.deserialize(None).map_err(convert::Into::into);
142 }
143 Some("D") => {
144 let row_len = record.len();
145 if row_len < 5 {
146 return Err(Error::TooShortRow(row_len));
147 }
148 let file: String = record[1].into();
149 let sub_file: String = record[2].into();
150 let sub_file_version: i32 = record[3].parse()?;
151
152 let rest_record =
154 record
155 .into_iter()
156 .skip(4)
157 .fold(csv::StringRecord::new(), |mut acc, x| {
158 acc.push_field(x);
159 acc
160 });
161
162 if let Some((k, mut v)) =
163 data.remove_entry(&(file.clone(), sub_file.clone(), sub_file_version))
164 {
165 v.push(rest_record);
166 data.insert(k, v);
167 } else {
168 data.insert(
169 (file.clone(), sub_file.clone(), sub_file_version),
170 vec![rest_record],
171 );
172 }
173
174 }
179 Some("I") => continue, Some(t) => return Err(Error::UnexpectedRowType(t.into())), None => return Err(Error::EmptyRow),
182 }
183 }
184 let expected_line_count = footer?.line_count_inclusive;
186
187 let file = Self { header, data };
188
189 let data_rows = file.data.iter().fold(0, |acc, (_, v)| acc + 1 + v.len());
190
191 if data_rows + 2 == expected_line_count {
192 Ok(file)
193 } else {
194 Err(Error::IncorrectLineCount {
195 got: data_rows + 2,
196 expected: expected_line_count,
197 })
198 }
199 }
200}
201
202pub trait FileKeyable {
203 fn key() -> FileKey;
204}
205
206pub trait GetFromRawAemo {
207 type Output: FileKeyable + serde::de::DeserializeOwned;
208 fn from_map(
209 data: &mut collections::HashMap<FileKey, Vec<csv::StringRecord>>,
210 ) -> Result<Vec<Self::Output>> {
211 let key = &Self::Output::key();
212 info!("Extracting file {:?}", key);
213 data.remove_entry(key)
214 .ok_or_else(|| Error::MissingFile(Self::Output::key()))?
215 .1
216 .into_iter()
217 .map(|rec| rec.deserialize(None))
218 .collect::<std::result::Result<Vec<Self::Output>, csv::Error>>()
219 .map_err(convert::Into::into)
220 }
221}
222
223pub trait AemoFile: Sized + Send {
224 fn from_raw(raw: RawAemoFile) -> Result<Self>;
225}
226
227fn au_datetime_deserialize<'de, D>(d: D) -> std::result::Result<chrono::NaiveDateTime, D::Error>
228where
229 D: serde::Deserializer<'de>,
230{
231 let s = serde::Deserialize::deserialize(d)?;
232 chrono::NaiveDateTime::parse_from_str(s, "%Y/%m/%d %H:%M:%S").map_err(de::Error::custom)
233}
234
235fn opt_au_datetime_deserialize<'de, D>(
236 d: D,
237) -> std::result::Result<Option<chrono::NaiveDateTime>, D::Error>
238where
239 D: serde::Deserializer<'de>,
240{
241 let a_str: &'de str = serde::Deserialize::deserialize(d)?;
242 if a_str.len() == 0 {
243 Ok(None)
244 } else {
245 chrono::NaiveDateTime::parse_from_str(a_str, "%Y/%m/%d %H:%M:%S")
246 .map_err(D::Error::custom)
247 .map(Some)
248 }
249}
250
251fn au_date_deserialize<'de, D>(d: D) -> std::result::Result<chrono::NaiveDate, D::Error>
252where
253 D: serde::Deserializer<'de>,
254{
255 let s = serde::Deserialize::deserialize(d)?;
256 chrono::NaiveDate::parse_from_str(s, "%Y/%m/%d").map_err(de::Error::custom)
257}
258
259fn au_time_deserialize<'de, D>(d: D) -> std::result::Result<chrono::NaiveTime, D::Error>
260where
261 D: serde::Deserializer<'de>,
262{
263 let s = serde::Deserialize::deserialize(d)?;
264 chrono::NaiveTime::parse_from_str(s, "%H:%M:%S").map_err(de::Error::custom)
265}