Skip to main content

datui_lib/home/
codebook.rs

1//! A catalog dataset's documentation: what its columns mean, in their publisher's words.
2//!
3//! Long, coded datasets (weather stations, trip records) are unreadable without one.
4//! A catalog entry carries it as `documentation` (the link) and `columns`; the Info
5//! panel, the inspector and the home screen's details read it from here.
6
7use crate::home::catalog::Dataset;
8use std::collections::BTreeMap;
9
10/// One column's note.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct Column {
13    pub description: String,
14    pub unit: String,
15    /// Code to meaning; `""` is what a blank or null value means.
16    pub values: BTreeMap<String, String>,
17}
18
19impl Column {
20    /// The description and the unit on one line.
21    pub fn about(&self) -> String {
22        match (self.description.is_empty(), self.unit.is_empty()) {
23            (false, false) => format!("{} ({})", self.description, self.unit),
24            (false, true) => self.description.clone(),
25            (true, false) => self.unit.clone(),
26            (true, true) => String::new(),
27        }
28    }
29
30    /// What `value` stands for, by its exact code: GHCN's source flags `a` and `A`
31    /// are different sources. `None` (a null) and a blank value read the `""` entry.
32    pub fn meaning(&self, value: Option<&str>) -> Option<(&str, &str)> {
33        let code = value.map(str::trim).unwrap_or("");
34        self.values
35            .get_key_value(code)
36            .map(|(key, meaning)| (key.as_str(), meaning.as_str()))
37    }
38
39    /// The legend line for `value`: `S = failed spatial consistency check`, or
40    /// `blank = did not fail any quality assurance check`.
41    pub fn legend_line(&self, value: Option<&str>) -> Option<String> {
42        let (code, meaning) = self.meaning(value)?;
43        let code = if code.is_empty() { "blank" } else { code };
44        Some(format!("{code} = {meaning}"))
45    }
46}
47
48/// A dataset's codebook.
49#[derive(Debug, Clone, Default, PartialEq, Eq)]
50pub struct Codebook {
51    /// The publisher's documentation the notes come from.
52    pub source: String,
53    pub columns: BTreeMap<String, Column>,
54}
55
56impl Codebook {
57    /// The column notes a catalog entry carries, when it carries any.
58    pub fn of(dataset: &Dataset) -> Option<Self> {
59        if dataset.columns.is_empty() {
60            return None;
61        }
62        Some(Self {
63            source: dataset.documentation.clone(),
64            columns: dataset
65                .columns
66                .iter()
67                .map(|(name, note)| {
68                    (
69                        name.clone(),
70                        Column {
71                            description: note.description.clone(),
72                            unit: note.unit.clone(),
73                            values: note.values.iter().cloned().collect(),
74                        },
75                    )
76                })
77                .collect(),
78        })
79    }
80
81    pub fn column(&self, name: &str) -> Option<&Column> {
82        self.columns.get(name)
83    }
84
85    /// Whether any of `names` has a note.
86    pub fn covers<'a>(&self, mut names: impl Iterator<Item = &'a str>) -> bool {
87        names.any(|name| self.columns.contains_key(name))
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn flag() -> Column {
96        Column {
97            description: "Quality flag".to_string(),
98            unit: String::new(),
99            values: [
100                ("", "did not fail any quality assurance check"),
101                ("S", "failed spatial consistency check"),
102            ]
103            .into_iter()
104            .map(|(k, v)| (k.to_string(), v.to_string()))
105            .collect(),
106        }
107    }
108
109    #[test]
110    fn a_code_reads_its_meaning_and_blank_reads_the_empty_entry() {
111        let column = flag();
112        assert_eq!(
113            column.legend_line(Some("S")).as_deref(),
114            Some("S = failed spatial consistency check")
115        );
116        assert_eq!(column.legend_line(Some("s")), None);
117        assert_eq!(
118            column.legend_line(None).as_deref(),
119            Some("blank = did not fail any quality assurance check")
120        );
121        assert_eq!(
122            column.legend_line(Some(" ")).as_deref(),
123            Some("blank = did not fail any quality assurance check")
124        );
125        assert_eq!(column.legend_line(Some("Q")), None);
126        assert_eq!(column.about(), "Quality flag");
127    }
128}