bufkit_data/
archive.rs

1//! An archive of bufkit soundings.
2
3use crate::{coords::Coords, errors::BufkitDataErr, site::StationNumber};
4
5#[cfg(feature = "pylib")]
6use pyo3::prelude::*;
7
8use std::convert::TryFrom;
9
10/// The archive.
11#[cfg_attr(feature = "pylib", pyclass(module = "bufkit_data"))]
12#[derive(Debug)]
13pub struct Archive {
14    root: std::path::PathBuf,      // The root directory.
15    db_conn: rusqlite::Connection, // An sqlite connection.
16}
17
18mod clean;
19mod modify;
20
21mod query;
22pub use query::StationSummary;
23
24mod root;
25
26struct InternalSiteInfo {
27    station_num: StationNumber,
28    id: Option<String>,
29    init_time: chrono::NaiveDateTime,
30    end_time: chrono::NaiveDateTime,
31    coords: Coords,
32    elevation: metfor::Meters,
33}
34
35impl Archive {
36    fn parse_site_info(text: &str) -> Result<InternalSiteInfo, BufkitDataErr> {
37        let bdata = sounding_bufkit::BufkitData::init(text, "")?;
38        let mut iter = bdata.into_iter();
39
40        let first = iter.next().ok_or(BufkitDataErr::NotEnoughData)?.0;
41        let last = iter.last().ok_or(BufkitDataErr::NotEnoughData)?.0;
42
43        let init_time: chrono::NaiveDateTime =
44            first.valid_time().ok_or(BufkitDataErr::MissingValidTime)?;
45        let end_time: chrono::NaiveDateTime =
46            last.valid_time().ok_or(BufkitDataErr::MissingValidTime)?;
47        let coords: Coords = first
48            .station_info()
49            .location()
50            .map(Coords::from)
51            .ok_or(BufkitDataErr::MissingStationData)?;
52
53        let elevation = match first.station_info().elevation().into_option() {
54            Some(elev) => elev,
55            None => return Err(BufkitDataErr::MissingStationData),
56        };
57
58        let station_num: i32 = first
59            .station_info()
60            .station_num()
61            .ok_or(BufkitDataErr::MissingStationData)?;
62        let station_num: StationNumber = u32::try_from(station_num)
63            .map_err(|_| BufkitDataErr::GeneralError("negative station number?".to_owned()))
64            .map(StationNumber::from)?;
65
66        Ok(InternalSiteInfo {
67            station_num,
68            id: first
69                .station_info()
70                .station_id()
71                .map(|id| id.to_uppercase()),
72            init_time,
73            end_time,
74            coords,
75            elevation,
76        })
77    }
78}
79
80#[cfg(test)]
81mod unit {
82    use super::*;
83    use crate::{Model, SiteInfo, StateProv, StationNumber};
84
85    use tempdir::TempDir;
86
87    // struct to hold temporary data for tests.
88    pub(super) struct TestArchive {
89        pub tmp: TempDir,
90        pub arch: Archive,
91    }
92
93    // Function to create a new archive to test.
94    pub(super) fn create_test_archive() -> Result<TestArchive, BufkitDataErr> {
95        let tmp = TempDir::new("bufkit-data-test-archive")?;
96        let arch = Archive::create(&tmp.path())?;
97
98        Ok(TestArchive { tmp, arch })
99    }
100
101    // Get some simplified data for testing.
102    pub(super) fn get_test_data() -> [(String, Model, String); 7] {
103        [
104            (
105                "KMSO".to_owned(),
106                Model::NAM,
107                include_str!("../example_data/2017040100Z_nam_kmso.buf").to_owned(),
108            ),
109            (
110                "KMSO".to_owned(),
111                Model::GFS,
112                include_str!("../example_data/2017040106Z_gfs3_kmso.buf").to_owned(),
113            ),
114            (
115                "KMSO".to_owned(),
116                Model::GFS,
117                include_str!("../example_data/2017040112Z_gfs3_kmso.buf").to_owned(),
118            ),
119            (
120                "KMSO".to_owned(),
121                Model::NAM,
122                include_str!("../example_data/2017040112Z_nam_kmso.buf").to_owned(),
123            ),
124            (
125                "KMSO".to_owned(),
126                Model::GFS,
127                include_str!("../example_data/2017040118Z_gfs3_kmso.buf").to_owned(),
128            ),
129            (
130                "KMSO".to_owned(),
131                Model::GFS,
132                include_str!("../example_data/2017040118Z_gfs_kmso.buf").to_owned(),
133            ),
134            (
135                "KMSO".to_owned(),
136                Model::NAM,
137                include_str!("../example_data/2017040118Z_namm_kmso.buf").to_owned(),
138            ),
139        ]
140    }
141
142    // Function to fill the archive with some example data.
143    pub(super) fn fill_test_archive(arch: &mut Archive) {
144        for (site, model, raw_data) in get_test_data().iter() {
145            match arch.add(site, None, None, *model, raw_data) {
146                Ok(_) => {}
147                Err(err) => {
148                    println!("{:?}", err);
149                    panic!("Test archive error filling.");
150                }
151            }
152        }
153    }
154
155    // A handy set of sites to use when testing sites.
156    pub(super) fn get_test_sites() -> [SiteInfo; 3] {
157        [
158            SiteInfo {
159                station_num: StationNumber::from(1),
160                name: Some("Chicago/O'Hare".to_owned()),
161                notes: Some("Major air travel hub.".to_owned()),
162                state: Some(StateProv::IL),
163                time_zone: None,
164            },
165            SiteInfo {
166                station_num: StationNumber::from(2),
167                name: Some("Seattle".to_owned()),
168                notes: Some("A coastal city with coffe and rain".to_owned()),
169                state: Some(StateProv::WA),
170                time_zone: Some(chrono::FixedOffset::west_opt(8 * 3600).unwrap()),
171            },
172            SiteInfo {
173                station_num: StationNumber::from(3),
174                name: Some("Missoula".to_owned()),
175                notes: Some("In a valley.".to_owned()),
176                state: None,
177                time_zone: Some(chrono::FixedOffset::west_opt(7 * 3600).unwrap()),
178            },
179        ]
180    }
181
182    #[test]
183    fn test_sites_round_trip() {
184        let TestArchive { tmp: _tmp, arch } =
185            create_test_archive().expect("Failed to create test archive.");
186
187        let test_sites = &get_test_sites();
188
189        for site in test_sites {
190            arch.add_site(site).expect("Error adding site.");
191        }
192
193        let retrieved_sites = arch.sites().expect("Error retrieving sites.");
194
195        for site in retrieved_sites {
196            println!("{:#?}", site);
197            assert!(test_sites
198                .iter()
199                .find(|st| st.station_num == site.station_num)
200                .is_some());
201        }
202    }
203
204    #[test]
205    fn test_files_round_trip() {
206        let TestArchive { tmp: _tmp, arch } =
207            create_test_archive().expect("Failed to create test archive.");
208
209        let test_data = get_test_data();
210
211        for (site, model, raw_data) in test_data.iter() {
212            let init_time = sounding_bufkit::BufkitData::init(&raw_data, "x")
213                .unwrap()
214                .into_iter()
215                .next()
216                .unwrap()
217                .0
218                .valid_time()
219                .unwrap();
220
221            dbg!(init_time);
222            dbg!(&site);
223
224            let site = match arch.add(site, None, None, *model, raw_data) {
225                Ok(site) => site,
226                x => panic!("Error adding site: {:?}", x),
227            };
228
229            dbg!(&site);
230
231            let recovered_str = arch
232                .retrieve(site, *model, init_time)
233                .expect("Failure to load");
234
235            assert!(raw_data == &recovered_str);
236        }
237    }
238
239    #[test]
240    fn test_adding_duplicates() {
241        let TestArchive {
242            tmp: _tmp,
243            mut arch,
244        } = create_test_archive().expect("Failed to create test archive.");
245
246        let kmso = StationNumber::from(727730); // Station number for KMSO
247
248        fill_test_archive(&mut arch);
249
250        assert_eq!(
251            arch.inventory(kmso, Model::GFS)
252                .expect("db error")
253                .iter()
254                .count(),
255            3
256        );
257        assert_eq!(
258            arch.inventory(kmso, Model::NAM)
259                .expect("db error")
260                .iter()
261                .count(),
262            3
263        );
264
265        // Do it again and make sure the numbers are the same.
266        fill_test_archive(&mut arch);
267
268        assert_eq!(
269            arch.inventory(kmso, Model::GFS)
270                .expect("db error")
271                .iter()
272                .count(),
273            3
274        );
275        assert_eq!(
276            arch.inventory(kmso, Model::NAM)
277                .expect("db error")
278                .iter()
279                .count(),
280            3
281        );
282    }
283}