grib_build/
grib2_codeflag_csv.rs

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}