1use std::{collections::BTreeMap, error::Error, fmt, fs::File, path::Path, str::FromStr};
2
3use serde::Deserialize;
4
5use crate::CodeTable;
6
7#[derive(Debug, Deserialize)]
8struct Record {
9 #[serde(rename = "Title_en")]
10 title: String,
11 #[serde(rename = "SubTitle_en")]
12 subtitle: String,
13 #[serde(rename = "CodeFlag")]
14 code_flag: String,
15 #[allow(dead_code)]
16 #[serde(rename = "Value")]
17 value: String,
18 #[serde(rename = "MeaningParameterDescription_en")]
19 description: String,
20 #[allow(dead_code)]
21 #[serde(rename = "Note_en")]
22 note: String,
23 #[allow(dead_code)]
24 #[serde(rename = "UnitComments_en")]
25 unit: String,
26 #[allow(dead_code)]
27 #[serde(rename = "Status")]
28 status: String,
29}
30
31pub struct CodeDB {
32 data: BTreeMap<(u8, u8, OptArg), CodeTable>,
33}
34
35impl Default for CodeDB {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl CodeDB {
42 pub fn new() -> Self {
43 Self {
44 data: BTreeMap::new(),
45 }
46 }
47
48 pub fn load<P>(&mut self, path: P) -> Result<(), Box<dyn Error>>
49 where
50 P: AsRef<Path>,
51 {
52 let basename = path
53 .as_ref()
54 .file_stem()
55 .ok_or("unexpected path")?
56 .to_string_lossy();
57 let words: Vec<_> = basename.split('_').take(4).collect();
58 if let ["GRIB2", "CodeFlag", section, number] = words[..] {
59 let section = section.parse::<u8>()?;
60 let number = number.parse::<u8>()?;
61 for (category, table) in Self::parse_file(path)? {
62 self.data.insert((section, number, category), table);
63 }
64 };
65
66 Ok(())
67 }
68
69 fn parse_file<P>(path: P) -> Result<Vec<(OptArg, CodeTable)>, Box<dyn Error>>
70 where
71 P: AsRef<Path>,
72 {
73 let f = File::open(path)?;
74 let mut reader = csv::Reader::from_reader(f);
75 let mut out_tables = Vec::<(OptArg, CodeTable)>::new();
76
77 for record in reader.deserialize() {
78 let record: Record = record?;
79 let category = record.subtitle.parse::<OptArg>()?;
80
81 let current = out_tables.last();
82 if current.is_none() || category != current.unwrap().0 {
83 let new_codetable = CodeTable::new(record.title);
84 out_tables.push((category, new_codetable));
85 }
86
87 out_tables
88 .last_mut()
89 .unwrap()
90 .1
91 .data
92 .push((record.code_flag, record.description));
93 }
94
95 Ok(out_tables)
96 }
97
98 pub fn export(&self, id: (u8, u8, OptArg)) -> String {
99 match self.get(id) {
100 Some(code_table) => {
101 let variable_name = self.get_variable_name(id);
102 code_table.export(&variable_name)
103 }
104 None => "[]".to_string(),
105 }
106 }
107
108 fn get_variable_name(&self, id: (u8, u8, OptArg)) -> String {
109 match id {
110 (section, number, OptArg::None) => format!("CODE_TABLE_{section}_{number}"),
111 (section, number, OptArg::L1(discipline)) => {
112 format!("CODE_TABLE_{section}_{number}_{discipline}")
113 }
114 (section, number, OptArg::L2(discipline, category)) => {
115 format!("CODE_TABLE_{section}_{number}_{discipline}_{category}")
116 }
117 }
118 }
119
120 pub fn get(&self, id: (u8, u8, OptArg)) -> Option<&CodeTable> {
121 self.data.get(&id)
122 }
123}
124
125impl fmt::Display for CodeDB {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 let mut first = true;
128 for (id, code_table) in &self.data {
129 if first {
130 first = false;
131 } else {
132 write!(f, "\n\n")?;
133 }
134
135 let variable_name = self.get_variable_name(*id);
136 write!(f, "{}", code_table.export(&variable_name))?;
137 }
138 Ok(())
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
143pub enum OptArg {
144 None,
145 L1(u8),
146 L2(u8, u8),
147}
148
149impl FromStr for OptArg {
150 type Err = ParseError;
151 fn from_str(s: &str) -> Result<Self, Self::Err> {
152 match s {
153 "" => Ok(OptArg::None),
154 s => {
155 let mut splitted = s.split(", ");
156
157 let first = splitted.next().ok_or(ParseError)?;
158 let words: Vec<_> = first.split(' ').take(3).collect();
159 let discipline = match words[..] {
160 ["Product", "discipline", num] => u8::from_str(num).map_err(|_| ParseError),
161 _ => Err(ParseError),
162 }?;
163
164 if let Some(second) = splitted.next() {
165 let second = second.split(':').next().ok_or(ParseError)?;
166 let words: Vec<_> = second.split(' ').take(3).collect();
167 let parameter = match words[..] {
168 ["parameter", "category", num] => u8::from_str(num).map_err(|_| ParseError),
169 _ => Err(ParseError),
170 }?;
171
172 Ok(OptArg::L2(discipline, parameter))
173 } else {
174 Ok(OptArg::L1(discipline))
175 }
176 }
177 }
178 }
179}
180
181#[derive(Debug)]
182pub struct ParseError;
183
184impl fmt::Display for ParseError {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 write!(f, "ParseError")
187 }
188}
189
190impl Error for ParseError {
191 fn source(&self) -> Option<&(dyn Error + 'static)> {
192 None
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 const PATH_STR_0: &str = "testdata/GRIB2_CodeFlag_0_0_CodeTable_no_subtitle.csv";
201 const PATH_STR_1: &str = "testdata/GRIB2_CodeFlag_1_0_CodeTable_no_subtitle.csv";
202 const PATH_STR_2: &str = "testdata/GRIB2_CodeFlag_4_1_CodeTable_with_subtitle.csv";
203 const PATH_STR_3: &str = "testdata/GRIB2_CodeFlag_4_2_CodeTable_with_subtitle.csv";
204
205 #[test]
206 fn parse_subtitle() {
207 let string =
208 "Product discipline 0 - Meteorological products, parameter category 0: temperature";
209 let category = string.parse::<OptArg>().unwrap();
210 assert_eq!(category, OptArg::L2(0, 0));
211 }
212
213 #[test]
214 fn parse_file_no_subtitle() {
215 let tables = CodeDB::parse_file(PATH_STR_0).unwrap();
216
217 let expected_title = "Foo".to_owned();
218 let expected_data = vec![(
219 OptArg::None,
220 vec![
221 ("0", "0A"),
222 ("1", "0B"),
223 ("2-254", "Reserved"),
224 ("255", "Missing"),
225 ],
226 )];
227
228 let expected = expected_data
229 .iter()
230 .map(|(c, table)| {
231 (
232 *c,
233 CodeTable {
234 desc: expected_title.clone(),
235 data: table
236 .iter()
237 .map(|(a, b)| (a.to_string(), b.to_string()))
238 .collect::<Vec<_>>(),
239 },
240 )
241 })
242 .collect::<Vec<_>>();
243
244 assert_eq!(tables, expected);
245 }
246
247 #[test]
248 fn parse_file_with_subtitle_l1() {
249 let tables = CodeDB::parse_file(PATH_STR_2).unwrap();
250
251 let expected_title = "Baz".to_owned();
252 let expected_data = vec![
253 (
254 OptArg::L1(0),
255 vec![
256 ("0", "Temperature"),
257 ("1-2", "Reserved"),
258 ("3", "Mass"),
259 ("4-191", "Reserved"),
260 ("192-254", "Reserved for local use"),
261 ("255", "Missing"),
262 ],
263 ),
264 (
265 OptArg::L1(3),
266 vec![
267 ("0", "Image format products"),
268 ("1", "Quantitative products"),
269 ("2", "Cloud properties"),
270 ("3-191", "Reserved"),
271 ("192-254", "Reserved for local use"),
272 ("255", "Missing"),
273 ],
274 ),
275 (
276 OptArg::L1(20),
277 vec![
278 ("0", "Health indicators"),
279 ("1-191", "Reserved"),
280 ("192-254", "Reserved for local use"),
281 ("255", "Missing"),
282 ],
283 ),
284 ];
285
286 let expected = expected_data
287 .iter()
288 .map(|(c, table)| {
289 (
290 *c,
291 CodeTable {
292 desc: expected_title.clone(),
293 data: table
294 .iter()
295 .map(|(a, b)| (a.to_string(), b.to_string()))
296 .collect::<Vec<_>>(),
297 },
298 )
299 })
300 .collect::<Vec<_>>();
301
302 assert_eq!(tables, expected);
303 }
304
305 #[test]
306 fn parse_file_with_subtitle_l2() {
307 let tables = CodeDB::parse_file(PATH_STR_3).unwrap();
308
309 let expected_title = "Baz".to_owned();
310 let expected_data = vec![
311 (
312 OptArg::L2(0, 0),
313 vec![
314 ("0", "Temperature"),
315 ("1-9", "Reserved"),
316 ("10", "Latent heat net flux"),
317 ("11-191", "Reserved"),
318 ("192-254", "Reserved for local use"),
319 ("255", "Missing"),
320 ],
321 ),
322 (
323 OptArg::L2(0, 191),
324 vec![
325 (
326 "0",
327 "Seconds prior to initial reference time (defined in Section 1)",
328 ),
329 ("1-191", "Reserved"),
330 ("192-254", "Reserved for local use"),
331 ("255", "Missing"),
332 ],
333 ),
334 (
335 OptArg::L2(3, 2),
336 vec![("0", "Clear sky probability"), ("30", "Measurement cost")],
337 ),
338 (
339 OptArg::L2(20, 0),
340 vec![
341 ("0", "Universal thermal climate index"),
342 ("1-191", "Reserved"),
343 ("192-254", "Reserved for local use"),
344 ("255", "Missing"),
345 ],
346 ),
347 ];
348
349 let expected = expected_data
350 .iter()
351 .map(|(c, table)| {
352 (
353 *c,
354 CodeTable {
355 desc: expected_title.clone(),
356 data: table
357 .iter()
358 .map(|(a, b)| (a.to_string(), b.to_string()))
359 .collect::<Vec<_>>(),
360 },
361 )
362 })
363 .collect::<Vec<_>>();
364
365 assert_eq!(tables, expected);
366 }
367
368 #[test]
369 fn export() {
370 let mut db = CodeDB::new();
371 db.load(PATH_STR_0).unwrap();
372 assert_eq!(
373 db.export((0, 0, OptArg::None)),
374 "\
375/// Foo
376const CODE_TABLE_0_0: &[& str] = &[
377 \"0A\",
378 \"0B\",
379];"
380 );
381 }
382
383 #[test]
384 fn format() {
385 let mut db = CodeDB::new();
386 db.load(PATH_STR_0).unwrap();
387 db.load(PATH_STR_1).unwrap();
388 db.load(PATH_STR_2).unwrap();
389 db.load(PATH_STR_3).unwrap();
390 assert_eq!(
391 format!("{db}"),
392 "\
393/// Foo
394const CODE_TABLE_0_0: &[& str] = &[
395 \"0A\",
396 \"0B\",
397];
398
399/// Bar
400const CODE_TABLE_1_0: &[& str] = &[
401 \"1A\",
402 \"1B\",
403];
404
405/// Baz
406const CODE_TABLE_4_1_0: &[& str] = &[
407 \"Temperature\",
408 \"\",
409 \"\",
410 \"Mass\",
411];
412
413/// Baz
414const CODE_TABLE_4_1_3: &[& str] = &[
415 \"Image format products\",
416 \"Quantitative products\",
417 \"Cloud properties\",
418];
419
420/// Baz
421const CODE_TABLE_4_1_20: &[& str] = &[
422 \"Health indicators\",
423];
424
425/// Baz
426const CODE_TABLE_4_2_0_0: &[& str] = &[
427 \"Temperature\",
428 \"\",
429 \"\",
430 \"\",
431 \"\",
432 \"\",
433 \"\",
434 \"\",
435 \"\",
436 \"\",
437 \"Latent heat net flux\",
438];
439
440/// Baz
441const CODE_TABLE_4_2_0_191: &[& str] = &[
442 \"Seconds prior to initial reference time (defined in Section 1)\",
443];
444
445/// Baz
446const CODE_TABLE_4_2_3_2: &[& str] = &[];
447
448/// Baz
449const CODE_TABLE_4_2_20_0: &[& str] = &[
450 \"Universal thermal climate index\",
451];"
452 );
453 }
454
455 #[test]
456 fn codetable_to_vec() {
457 let mut db = CodeDB::new();
458 db.load(PATH_STR_0).unwrap();
459 assert_eq!(
460 db.get((0, 0, OptArg::None)).unwrap().to_vec(),
461 vec!["0A", "0B",]
462 );
463 }
464}