Skip to main content

rust_hdf5/
group.rs

1//! Group support.
2//!
3//! Groups are containers for datasets and other groups, forming a
4//! hierarchical namespace within an HDF5 file.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use rust_hdf5::H5File;
10//!
11//! let file = H5File::create("groups.h5").unwrap();
12//! let root = file.root_group();
13//! let grp = root.create_group("detector").unwrap();
14//! let ds = grp.new_dataset::<f32>()
15//!     .shape(&[10])
16//!     .create("temperature")
17//!     .unwrap();
18//! ```
19
20use crate::dataset::DatasetBuilder;
21use crate::error::{Hdf5Error, Result};
22use crate::file::{borrow_inner, borrow_inner_mut, clone_inner, H5FileInner, SharedInner};
23use crate::types::H5Type;
24
25/// A handle to an HDF5 group.
26///
27/// Groups are containers for datasets and other groups. The root group
28/// is always available via [`H5File::root_group`](crate::file::H5File::root_group).
29pub struct H5Group {
30    file_inner: SharedInner,
31    /// The absolute path of this group (e.g., "/" or "/detector").
32    name: String,
33}
34
35impl H5Group {
36    /// Create a new group handle.
37    pub(crate) fn new(file_inner: SharedInner, name: String) -> Self {
38        Self { file_inner, name }
39    }
40
41    /// Return the name (path) of this group.
42    pub fn name(&self) -> &str {
43        &self.name
44    }
45
46    /// Start building a new dataset in this group.
47    ///
48    /// The dataset will be registered as a child of this group in the
49    /// HDF5 file hierarchy.
50    pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
51        DatasetBuilder::new_in_group(clone_inner(&self.file_inner), self.name.clone())
52    }
53
54    /// Create a sub-group within this group.
55    ///
56    /// Creates a real HDF5 group with its own object header.
57    pub fn create_group(&self, name: &str) -> Result<H5Group> {
58        let full_name = if self.name == "/" {
59            format!("/{}", name)
60        } else {
61            format!("{}/{}", self.name, name)
62        };
63
64        let mut inner = borrow_inner_mut(&self.file_inner);
65        match &mut *inner {
66            H5FileInner::Writer(writer) => {
67                writer.create_group(&self.name, name)?;
68            }
69            H5FileInner::Reader(_) => {
70                return Err(Hdf5Error::InvalidState(
71                    "cannot create groups in read mode".into(),
72                ));
73            }
74            H5FileInner::Closed => {
75                return Err(Hdf5Error::InvalidState("file is closed".into()));
76            }
77        }
78        drop(inner);
79
80        Ok(H5Group {
81            file_inner: clone_inner(&self.file_inner),
82            name: full_name,
83        })
84    }
85
86    /// Open an existing sub-group by name (read mode).
87    pub fn group(&self, name: &str) -> Result<H5Group> {
88        let full_name = if self.name == "/" {
89            format!("/{}", name)
90        } else {
91            format!("{}/{}", self.name, name)
92        };
93
94        // Verify the group exists by checking if any datasets have this prefix
95        let inner = borrow_inner(&self.file_inner);
96        if let H5FileInner::Reader(reader) = &*inner {
97            let prefix = if full_name == "/" {
98                String::new()
99            } else {
100                format!("{}/", full_name.trim_start_matches('/'))
101            };
102            let has_children = reader
103                .dataset_names()
104                .iter()
105                .any(|n| n.starts_with(&prefix));
106            if !has_children {
107                return Err(Hdf5Error::NotFound(full_name));
108            }
109        }
110        drop(inner);
111
112        Ok(H5Group {
113            file_inner: clone_inner(&self.file_inner),
114            name: full_name,
115        })
116    }
117
118    /// List dataset names that are direct children of this group.
119    pub fn dataset_names(&self) -> Result<Vec<String>> {
120        let inner = borrow_inner(&self.file_inner);
121        let all_names = match &*inner {
122            H5FileInner::Reader(reader) => reader
123                .dataset_names()
124                .iter()
125                .map(|s| s.to_string())
126                .collect::<Vec<_>>(),
127            H5FileInner::Writer(writer) => writer
128                .dataset_names()
129                .iter()
130                .map(|s| s.to_string())
131                .collect::<Vec<_>>(),
132            H5FileInner::Closed => return Ok(vec![]),
133        };
134
135        let prefix = if self.name == "/" {
136            String::new()
137        } else {
138            format!("{}/", self.name.trim_start_matches('/'))
139        };
140
141        let mut result = Vec::new();
142        for name in &all_names {
143            let stripped = if prefix.is_empty() {
144                name.as_str()
145            } else if let Some(rest) = name.strip_prefix(&prefix) {
146                rest
147            } else {
148                continue;
149            };
150            // Only direct children (no further '/')
151            if !stripped.contains('/') {
152                result.push(stripped.to_string());
153            }
154        }
155        Ok(result)
156    }
157
158    /// List sub-group names that are direct children of this group.
159    pub fn group_names(&self) -> Result<Vec<String>> {
160        let inner = borrow_inner(&self.file_inner);
161        let all_names = match &*inner {
162            H5FileInner::Reader(reader) => reader
163                .dataset_names()
164                .iter()
165                .map(|s| s.to_string())
166                .collect::<Vec<_>>(),
167            H5FileInner::Writer(writer) => writer
168                .dataset_names()
169                .iter()
170                .map(|s| s.to_string())
171                .collect::<Vec<_>>(),
172            H5FileInner::Closed => return Ok(vec![]),
173        };
174
175        let prefix = if self.name == "/" {
176            String::new()
177        } else {
178            format!("{}/", self.name.trim_start_matches('/'))
179        };
180
181        let mut groups = std::collections::BTreeSet::new();
182        for name in &all_names {
183            let stripped = if prefix.is_empty() {
184                name.as_str()
185            } else if let Some(rest) = name.strip_prefix(&prefix) {
186                rest
187            } else {
188                continue;
189            };
190            // If there's a '/', the first part is a group name
191            if let Some(pos) = stripped.find('/') {
192                groups.insert(stripped[..pos].to_string());
193            }
194        }
195        Ok(groups.into_iter().collect())
196    }
197}