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    /// Create a variable-length string dataset and write data within this group.
159    pub fn write_vlen_strings(&self, name: &str, strings: &[&str]) -> Result<()> {
160        let full_name = if self.name == "/" {
161            name.to_string()
162        } else {
163            let trimmed = self.name.trim_start_matches('/');
164            format!("{}/{}", trimmed, name)
165        };
166
167        let mut inner = borrow_inner_mut(&self.file_inner);
168        match &mut *inner {
169            H5FileInner::Writer(writer) => {
170                let idx = writer.create_vlen_string_dataset(&full_name, strings)?;
171                if self.name != "/" {
172                    writer.assign_dataset_to_group(&self.name, idx)?;
173                }
174                Ok(())
175            }
176            H5FileInner::Reader(_) => {
177                Err(Hdf5Error::InvalidState("cannot write in read mode".into()))
178            }
179            H5FileInner::Closed => Err(Hdf5Error::InvalidState("file is closed".into())),
180        }
181    }
182
183    /// List sub-group names that are direct children of this group.
184    pub fn group_names(&self) -> Result<Vec<String>> {
185        let inner = borrow_inner(&self.file_inner);
186        let all_names = match &*inner {
187            H5FileInner::Reader(reader) => reader
188                .dataset_names()
189                .iter()
190                .map(|s| s.to_string())
191                .collect::<Vec<_>>(),
192            H5FileInner::Writer(writer) => writer
193                .dataset_names()
194                .iter()
195                .map(|s| s.to_string())
196                .collect::<Vec<_>>(),
197            H5FileInner::Closed => return Ok(vec![]),
198        };
199
200        let prefix = if self.name == "/" {
201            String::new()
202        } else {
203            format!("{}/", self.name.trim_start_matches('/'))
204        };
205
206        let mut groups = std::collections::BTreeSet::new();
207        for name in &all_names {
208            let stripped = if prefix.is_empty() {
209                name.as_str()
210            } else if let Some(rest) = name.strip_prefix(&prefix) {
211                rest
212            } else {
213                continue;
214            };
215            // If there's a '/', the first part is a group name
216            if let Some(pos) = stripped.find('/') {
217                groups.insert(stripped[..pos].to_string());
218            }
219        }
220        Ok(groups.into_iter().collect())
221    }
222}