Skip to main content

netcdf_reader/nc4/
groups.rs

1//! Map HDF5 groups to NetCDF-4 groups.
2//!
3//! Each HDF5 group becomes an `NcGroup`. The root group is special: it may
4//! contain `_NCProperties` and other internal attributes that should be filtered.
5//! Sub-groups are traversed recursively.
6
7use std::collections::HashMap;
8
9use hdf5_reader::Hdf5File;
10
11use crate::error::Result;
12use crate::types::{NcDimension, NcGroup};
13
14use super::attributes;
15use super::dimensions;
16use super::variables;
17
18fn leaf_name(name: &str) -> &str {
19    name.rsplit('/').next().unwrap_or(name)
20}
21
22fn visible_dimensions(
23    local_dimensions: &[NcDimension],
24    inherited_dimensions: &[NcDimension],
25) -> Vec<NcDimension> {
26    let mut visible_dimensions = local_dimensions.to_vec();
27    visible_dimensions.extend(
28        inherited_dimensions
29            .iter()
30            .filter(|dim| {
31                !local_dimensions
32                    .iter()
33                    .any(|local_dim| local_dim.name == dim.name)
34            })
35            .cloned(),
36    );
37    visible_dimensions
38}
39
40fn visible_dim_addr_map(
41    local_dim_addr_map: HashMap<u64, NcDimension>,
42    inherited_dim_addr_map: &HashMap<u64, NcDimension>,
43) -> HashMap<u64, NcDimension> {
44    let mut visible_dim_addr_map = inherited_dim_addr_map.clone();
45    visible_dim_addr_map.extend(local_dim_addr_map);
46    visible_dim_addr_map
47}
48
49/// Build the root NcGroup from an HDF5 file.
50pub fn build_root_group(hdf5: &Hdf5File) -> Result<NcGroup> {
51    let root = hdf5.root_group()?;
52    build_group_recursive(&root, "/", &[], &HashMap::new())
53}
54
55/// Recursively build an NcGroup from an HDF5 Group.
56fn build_group_recursive(
57    hdf5_group: &hdf5_reader::group::Group<'_>,
58    name: &str,
59    inherited_dimensions: &[NcDimension],
60    inherited_dim_addr_map: &HashMap<u64, NcDimension>,
61) -> Result<NcGroup> {
62    let (hdf5_children, datasets) = hdf5_group.members()?;
63
64    // Extract dimensions declared locally in this group, then combine them
65    // with dimensions inherited from ancestor groups for lookups and variable
66    // reconstruction.
67    let (local_dimensions, local_dim_addr_map) =
68        dimensions::extract_dimensions_from_datasets(&datasets)?;
69    let visible_dimensions = visible_dimensions(&local_dimensions, inherited_dimensions);
70    let visible_dim_addr_map = visible_dim_addr_map(local_dim_addr_map, inherited_dim_addr_map);
71
72    // Extract variables (non-dimension-scale datasets).
73    let variables = variables::extract_variables_from_datasets(
74        &datasets,
75        hdf5_group,
76        &visible_dimensions,
77        &visible_dim_addr_map,
78    )?;
79
80    // Extract group-level attributes, filtering internal NetCDF-4 attributes.
81    let nc_attributes = attributes::extract_group_attributes(hdf5_group)?;
82
83    // Recurse into child groups.
84    let mut child_groups = Vec::new();
85    for child in &hdf5_children {
86        let child_name = leaf_name(child.name()).to_string();
87        let nc_child = build_group_recursive(
88            child,
89            &child_name,
90            &visible_dimensions,
91            &visible_dim_addr_map,
92        )?;
93        child_groups.push(nc_child);
94    }
95
96    Ok(NcGroup {
97        name: name.to_string(),
98        dimensions: visible_dimensions,
99        variables,
100        attributes: nc_attributes,
101        groups: child_groups,
102    })
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_visible_dimensions_include_inherited_without_duplicates() {
111        let local = vec![NcDimension {
112            name: "y".to_string(),
113            size: 4,
114            is_unlimited: false,
115        }];
116        let inherited = vec![
117            NcDimension {
118                name: "x".to_string(),
119                size: 3,
120                is_unlimited: false,
121            },
122            NcDimension {
123                name: "y".to_string(),
124                size: 99,
125                is_unlimited: true,
126            },
127        ];
128
129        let merged = visible_dimensions(&local, &inherited);
130        let names: Vec<&str> = merged.iter().map(|dim| dim.name.as_str()).collect();
131        assert_eq!(names, vec!["y", "x"]);
132        assert_eq!(merged[0].size, 4);
133        assert!(!merged[0].is_unlimited);
134    }
135
136    #[test]
137    fn test_visible_dim_addr_map_prefers_local_dimensions() {
138        let mut inherited = HashMap::new();
139        inherited.insert(
140            10,
141            NcDimension {
142                name: "x".to_string(),
143                size: 3,
144                is_unlimited: false,
145            },
146        );
147        inherited.insert(
148            20,
149            NcDimension {
150                name: "shared".to_string(),
151                size: 1,
152                is_unlimited: false,
153            },
154        );
155
156        let mut local = HashMap::new();
157        local.insert(
158            20,
159            NcDimension {
160                name: "shared".to_string(),
161                size: 2,
162                is_unlimited: true,
163            },
164        );
165
166        let merged = visible_dim_addr_map(local, &inherited);
167        assert_eq!(merged.len(), 2);
168        assert_eq!(merged.get(&10).unwrap().name, "x");
169        assert_eq!(merged.get(&20).unwrap().size, 2);
170        assert!(merged.get(&20).unwrap().is_unlimited);
171    }
172}