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::format::messages::filter::FilterPipeline;
24use crate::types::H5Type;
25
26/// A handle to an HDF5 group.
27///
28/// Groups are containers for datasets and other groups. The root group
29/// is always available via [`H5File::root_group`](crate::file::H5File::root_group).
30pub struct H5Group {
31    file_inner: SharedInner,
32    /// The absolute path of this group (e.g., "/" or "/detector").
33    name: String,
34}
35
36impl H5Group {
37    /// Create a new group handle.
38    pub(crate) fn new(file_inner: SharedInner, name: String) -> Self {
39        Self { file_inner, name }
40    }
41
42    /// Return the name (path) of this group.
43    pub fn name(&self) -> &str {
44        &self.name
45    }
46
47    /// Start building a new dataset in this group.
48    ///
49    /// The dataset will be registered as a child of this group in the
50    /// HDF5 file hierarchy.
51    pub fn new_dataset<T: H5Type>(&self) -> DatasetBuilder<T> {
52        DatasetBuilder::new_in_group(clone_inner(&self.file_inner), self.name.clone())
53    }
54
55    /// Create a sub-group within this group.
56    ///
57    /// Creates a real HDF5 group with its own object header.
58    pub fn create_group(&self, name: &str) -> Result<H5Group> {
59        let full_name = if self.name == "/" {
60            format!("/{}", name)
61        } else {
62            format!("{}/{}", self.name, name)
63        };
64
65        let mut inner = borrow_inner_mut(&self.file_inner);
66        match &mut *inner {
67            H5FileInner::Writer(writer) => {
68                writer.create_group(&self.name, name)?;
69            }
70            H5FileInner::Reader(_) => {
71                return Err(Hdf5Error::InvalidState(
72                    "cannot create groups in read mode".into(),
73                ));
74            }
75            H5FileInner::Closed => {
76                return Err(Hdf5Error::InvalidState("file is closed".into()));
77            }
78        }
79        drop(inner);
80
81        Ok(H5Group {
82            file_inner: clone_inner(&self.file_inner),
83            name: full_name,
84        })
85    }
86
87    /// Open an existing sub-group by name (read mode).
88    pub fn group(&self, name: &str) -> Result<H5Group> {
89        let full_name = if self.name == "/" {
90            format!("/{}", name)
91        } else {
92            format!("{}/{}", self.name, name)
93        };
94
95        // Verify the group exists by checking if any datasets have this prefix
96        let inner = borrow_inner(&self.file_inner);
97        if let H5FileInner::Reader(reader) = &*inner {
98            let prefix = if full_name == "/" {
99                String::new()
100            } else {
101                format!("{}/", full_name.trim_start_matches('/'))
102            };
103            let has_children = reader
104                .dataset_names()
105                .iter()
106                .any(|n| n.starts_with(&prefix));
107            if !has_children {
108                return Err(Hdf5Error::NotFound(full_name));
109            }
110        }
111        drop(inner);
112
113        Ok(H5Group {
114            file_inner: clone_inner(&self.file_inner),
115            name: full_name,
116        })
117    }
118
119    /// List dataset names that are direct children of this group.
120    pub fn dataset_names(&self) -> Result<Vec<String>> {
121        let inner = borrow_inner(&self.file_inner);
122        let all_names = match &*inner {
123            H5FileInner::Reader(reader) => reader
124                .dataset_names()
125                .iter()
126                .map(|s| s.to_string())
127                .collect::<Vec<_>>(),
128            H5FileInner::Writer(writer) => writer
129                .dataset_names()
130                .iter()
131                .map(|s| s.to_string())
132                .collect::<Vec<_>>(),
133            H5FileInner::Closed => return Ok(vec![]),
134        };
135
136        let prefix = if self.name == "/" {
137            String::new()
138        } else {
139            format!("{}/", self.name.trim_start_matches('/'))
140        };
141
142        let mut result = Vec::new();
143        for name in &all_names {
144            let stripped = if prefix.is_empty() {
145                name.as_str()
146            } else if let Some(rest) = name.strip_prefix(&prefix) {
147                rest
148            } else {
149                continue;
150            };
151            // Only direct children (no further '/')
152            if !stripped.contains('/') {
153                result.push(stripped.to_string());
154            }
155        }
156        Ok(result)
157    }
158
159    /// Create a variable-length string dataset and write data within this group.
160    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
161        let full_name = if self.name == "/" {
162            name.to_string()
163        } else {
164            let trimmed = self.name.trim_start_matches('/');
165            format!("{}/{}", trimmed, name)
166        };
167
168        let mut inner = borrow_inner_mut(&self.file_inner);
169        match &mut *inner {
170            H5FileInner::Writer(writer) => {
171                let idx = writer.create_vlen_string_dataset(&full_name, strings)?;
172                if self.name != "/" {
173                    writer.assign_dataset_to_group(&self.name, idx)?;
174                }
175                Ok(())
176            }
177            H5FileInner::Reader(_) => {
178                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
179            }
180            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
181        }
182    }
183
184    /// Create a chunked, compressed variable-length string dataset within this group.
185    pub fn write_vlen_strings_compressed(
186        &self,
187        name: &str,
188        strings: &[&str],
189        chunk_size: usize,
190        pipeline: FilterPipeline,
191    ) -> Result<()> {
192        let full_name = if self.name == "/" {
193            name.to_string()
194        } else {
195            let trimmed = self.name.trim_start_matches('/');
196            format!("{}/{}", trimmed, name)
197        };
198
199        let mut inner = borrow_inner_mut(&self.file_inner);
200        match &mut *inner {
201            H5FileInner::Writer(writer) => {
202                let idx = writer.create_vlen_string_dataset_compressed(
203                    &full_name, strings, chunk_size, pipeline,
204                )?;
205                if self.name != "/" {
206                    writer.assign_dataset_to_group(&self.name, idx)?;
207                }
208                Ok(())
209            }
210            H5FileInner::Reader(_) => {
211                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
212            }
213            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
214        }
215    }
216
217    /// List sub-group names that are direct children of this group.
218    pub fn group_names(&self) -> Result<Vec<String>> {
219        let inner = borrow_inner(&self.file_inner);
220        let all_names = match &*inner {
221            H5FileInner::Reader(reader) => reader
222                .dataset_names()
223                .iter()
224                .map(|s| s.to_string())
225                .collect::<Vec<_>>(),
226            H5FileInner::Writer(writer) => writer
227                .dataset_names()
228                .iter()
229                .map(|s| s.to_string())
230                .collect::<Vec<_>>(),
231            H5FileInner::Closed => return Ok(vec![]),
232        };
233
234        let prefix = if self.name == "/" {
235            String::new()
236        } else {
237            format!("{}/", self.name.trim_start_matches('/'))
238        };
239
240        let mut groups = std::collections::BTreeSet::new();
241        for name in &all_names {
242            let stripped = if prefix.is_empty() {
243                name.as_str()
244            } else if let Some(rest) = name.strip_prefix(&prefix) {
245                rest
246            } else {
247                continue;
248            };
249            // If there's a '/', the first part is a group name
250            if let Some(pos) = stripped.find('/') {
251                groups.insert(stripped[..pos].to_string());
252            }
253        }
254        Ok(groups.into_iter().collect())
255    }
256}