Skip to main content

oxigdal_netcdf/
reader.rs

1//! NetCDF file reader implementation.
2//!
3//! This module provides functionality for reading NetCDF files, including
4//! dimensions, variables, attributes, and data.
5
6use std::path::Path;
7
8use crate::attribute::{Attribute, AttributeValue, Attributes};
9use crate::dimension::{Dimension, Dimensions};
10use crate::error::{NetCdfError, Result};
11use crate::metadata::{CfMetadata, NetCdfMetadata, NetCdfVersion};
12use crate::variable::{DataType, Variable, Variables};
13
14#[cfg(feature = "netcdf3")]
15use std::cell::RefCell;
16
17/// NetCDF file reader.
18///
19/// Provides methods for reading NetCDF files, including metadata and data.
20pub struct NetCdfReader {
21    metadata: NetCdfMetadata,
22    #[cfg(feature = "netcdf3")]
23    file_nc3: Option<RefCell<netcdf3::FileReader>>,
24    #[cfg(feature = "netcdf4")]
25    file_nc4: Option<netcdf::File>,
26}
27
28impl std::fmt::Debug for NetCdfReader {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("NetCdfReader")
31            .field("metadata", &self.metadata)
32            .finish_non_exhaustive()
33    }
34}
35
36impl NetCdfReader {
37    /// Open a NetCDF file for reading.
38    ///
39    /// Automatically detects the file format (NetCDF-3 or NetCDF-4).
40    ///
41    /// # Arguments
42    ///
43    /// * `path` - Path to the NetCDF file
44    ///
45    /// # Errors
46    ///
47    /// Returns error if the file cannot be opened or read.
48    #[allow(unused_variables)]
49    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
50        let path = path.as_ref();
51
52        // Try NetCDF-3 first (Pure Rust)
53        #[cfg(feature = "netcdf3")]
54        {
55            if let Ok(file) = netcdf3::FileReader::open(path) {
56                return Self::from_netcdf3(file);
57            }
58        }
59
60        // Try NetCDF-4 if feature is enabled
61        #[cfg(feature = "netcdf4")]
62        {
63            if let Ok(file) = netcdf::open(path) {
64                return Self::from_netcdf4(file);
65            }
66        }
67
68        // If neither worked, return error
69        #[cfg(not(feature = "netcdf3"))]
70        #[cfg(not(feature = "netcdf4"))]
71        {
72            Err(NetCdfError::FeatureNotEnabled {
73                feature: "netcdf3 or netcdf4".to_string(),
74                message: "Enable 'netcdf3' or 'netcdf4' feature to read NetCDF files".to_string(),
75            })
76        }
77
78        #[cfg(feature = "netcdf3")]
79        #[cfg(not(feature = "netcdf4"))]
80        {
81            Err(NetCdfError::InvalidFormat(
82                "File is not a valid NetCDF-3 file".to_string(),
83            ))
84        }
85
86        #[cfg(feature = "netcdf4")]
87        #[cfg(not(feature = "netcdf3"))]
88        {
89            Err(NetCdfError::InvalidFormat(
90                "File is not a valid NetCDF-4 file".to_string(),
91            ))
92        }
93
94        #[cfg(feature = "netcdf3")]
95        #[cfg(feature = "netcdf4")]
96        {
97            Err(NetCdfError::InvalidFormat(
98                "File is not a valid NetCDF file".to_string(),
99            ))
100        }
101    }
102
103    /// Create a reader from a NetCDF-3 file.
104    ///
105    /// # Errors
106    ///
107    /// Returns error if metadata cannot be read.
108    #[cfg(feature = "netcdf3")]
109    pub fn from_netcdf3(file: netcdf3::FileReader) -> Result<Self> {
110        let metadata = Self::read_metadata_nc3(&file)?;
111        Ok(Self {
112            metadata,
113            file_nc3: Some(RefCell::new(file)),
114            #[cfg(feature = "netcdf4")]
115            file_nc4: None,
116        })
117    }
118
119    /// Create a reader from a NetCDF-4 file.
120    ///
121    /// # Errors
122    ///
123    /// Returns error if metadata cannot be read.
124    #[cfg(feature = "netcdf4")]
125    pub fn from_netcdf4(file: netcdf::File) -> Result<Self> {
126        let metadata = Self::read_metadata_nc4(&file)?;
127        Ok(Self {
128            metadata,
129            file_nc3: None,
130            file_nc4: Some(file),
131        })
132    }
133
134    /// Get the file metadata.
135    #[must_use]
136    pub const fn metadata(&self) -> &NetCdfMetadata {
137        &self.metadata
138    }
139
140    /// Get the file format version.
141    #[must_use]
142    pub fn version(&self) -> NetCdfVersion {
143        self.metadata.version()
144    }
145
146    /// Get dimensions.
147    #[must_use]
148    pub fn dimensions(&self) -> &Dimensions {
149        self.metadata.dimensions()
150    }
151
152    /// Get variables.
153    #[must_use]
154    pub fn variables(&self) -> &Variables {
155        self.metadata.variables()
156    }
157
158    /// Get global attributes.
159    #[must_use]
160    pub fn global_attributes(&self) -> &Attributes {
161        self.metadata.global_attributes()
162    }
163
164    /// Get CF metadata if available.
165    #[must_use]
166    pub fn cf_metadata(&self) -> Option<&CfMetadata> {
167        self.metadata.cf_metadata()
168    }
169
170    /// Read metadata from NetCDF-3 file.
171    #[cfg(feature = "netcdf3")]
172    fn read_metadata_nc3(file: &netcdf3::FileReader) -> Result<NetCdfMetadata> {
173        use crate::nc3_compat;
174
175        let mut metadata = NetCdfMetadata::new_classic();
176        let dataset = file.data_set();
177
178        // Read dimensions
179        let dimensions = nc3_compat::read_dimensions(dataset)?;
180        for dimension in dimensions {
181            metadata.dimensions_mut().add(dimension)?;
182        }
183
184        // Read global attributes
185        for attr_name in dataset.get_global_attr_names() {
186            if let Some(attr) = nc3_compat::read_global_attribute(dataset, &attr_name)? {
187                metadata.global_attributes_mut().add(attr)?;
188            }
189        }
190
191        // Read variables
192        for var_name in dataset.get_var_names() {
193            let var = nc3_compat::read_variable(dataset, &var_name)?;
194            metadata.variables_mut().add(var)?;
195        }
196
197        // Parse CF metadata
198        metadata.parse_cf_metadata();
199
200        Ok(metadata)
201    }
202
203    /// Convert NetCDF-3 data type to our data type.
204    #[cfg(feature = "netcdf3")]
205    fn convert_datatype_nc3(nc3_type: netcdf3::DataType) -> Result<DataType> {
206        use netcdf3::DataType as Nc3Type;
207
208        match nc3_type {
209            Nc3Type::I8 => Ok(DataType::I8),
210            Nc3Type::I16 => Ok(DataType::I16),
211            Nc3Type::I32 => Ok(DataType::I32),
212            Nc3Type::F32 => Ok(DataType::F32),
213            Nc3Type::F64 => Ok(DataType::F64),
214            Nc3Type::U8 => Ok(DataType::Char), // U8 in netcdf3 v0.6 represents character data
215        }
216    }
217
218    /// Read metadata from NetCDF-4 file.
219    #[cfg(feature = "netcdf4")]
220    fn read_metadata_nc4(_file: &netcdf::File) -> Result<NetCdfMetadata> {
221        // NetCDF-4 support is placeholder for now
222        Err(NetCdfError::NetCdf4NotAvailable)
223    }
224
225    /// Read variable data as f32.
226    ///
227    /// # Errors
228    ///
229    /// Returns error if variable not found or data cannot be read.
230    #[allow(unused_variables)]
231    pub fn read_f32(&self, var_name: &str) -> Result<Vec<f32>> {
232        #[cfg(feature = "netcdf3")]
233        if let Some(ref file_cell) = self.file_nc3 {
234            return Self::read_f32_nc3(&mut file_cell.borrow_mut(), var_name);
235        }
236
237        #[cfg(feature = "netcdf4")]
238        if let Some(ref _file) = self.file_nc4 {
239            return Err(NetCdfError::NetCdf4NotAvailable);
240        }
241
242        Err(NetCdfError::FeatureNotEnabled {
243            feature: "netcdf3 or netcdf4".to_string(),
244            message: "No reader available".to_string(),
245        })
246    }
247
248    /// Read variable data as f64.
249    ///
250    /// # Errors
251    ///
252    /// Returns error if variable not found or data cannot be read.
253    #[allow(unused_variables)]
254    pub fn read_f64(&self, var_name: &str) -> Result<Vec<f64>> {
255        #[cfg(feature = "netcdf3")]
256        if let Some(ref file_cell) = self.file_nc3 {
257            return Self::read_f64_nc3(&mut file_cell.borrow_mut(), var_name);
258        }
259
260        #[cfg(feature = "netcdf4")]
261        if let Some(ref _file) = self.file_nc4 {
262            return Err(NetCdfError::NetCdf4NotAvailable);
263        }
264
265        Err(NetCdfError::FeatureNotEnabled {
266            feature: "netcdf3 or netcdf4".to_string(),
267            message: "No reader available".to_string(),
268        })
269    }
270
271    /// Read variable data as i32.
272    ///
273    /// # Errors
274    ///
275    /// Returns error if variable not found or data cannot be read.
276    #[allow(unused_variables)]
277    pub fn read_i32(&self, var_name: &str) -> Result<Vec<i32>> {
278        #[cfg(feature = "netcdf3")]
279        if let Some(ref file_cell) = self.file_nc3 {
280            return Self::read_i32_nc3(&mut file_cell.borrow_mut(), var_name);
281        }
282
283        #[cfg(feature = "netcdf4")]
284        if let Some(ref _file) = self.file_nc4 {
285            return Err(NetCdfError::NetCdf4NotAvailable);
286        }
287
288        Err(NetCdfError::FeatureNotEnabled {
289            feature: "netcdf3 or netcdf4".to_string(),
290            message: "No reader available".to_string(),
291        })
292    }
293
294    /// Read f32 data from NetCDF-3 file.
295    #[cfg(feature = "netcdf3")]
296    fn read_f32_nc3(file: &mut netcdf3::FileReader, var_name: &str) -> Result<Vec<f32>> {
297        let dataset = file.data_set();
298        let var_info = dataset
299            .get_var(var_name)
300            .ok_or_else(|| NetCdfError::VariableNotFound {
301                name: var_name.to_string(),
302            })?;
303
304        use netcdf3::DataType as Nc3Type;
305        let data_type = var_info.data_type();
306
307        // Verify data type matches
308        if data_type != Nc3Type::F32 {
309            return Err(NetCdfError::DataTypeMismatch {
310                expected: "F32".to_string(),
311                found: format!("{:?}", data_type),
312            });
313        }
314
315        // Read the data using the mutable reference
316        let data = file.read_var_f32(var_name)?;
317        Ok(data)
318    }
319
320    /// Read f64 data from NetCDF-3 file.
321    #[cfg(feature = "netcdf3")]
322    fn read_f64_nc3(file: &mut netcdf3::FileReader, var_name: &str) -> Result<Vec<f64>> {
323        let dataset = file.data_set();
324        let var_info = dataset
325            .get_var(var_name)
326            .ok_or_else(|| NetCdfError::VariableNotFound {
327                name: var_name.to_string(),
328            })?;
329
330        use netcdf3::DataType as Nc3Type;
331        let data_type = var_info.data_type();
332
333        // Verify data type matches
334        if data_type != Nc3Type::F64 {
335            return Err(NetCdfError::DataTypeMismatch {
336                expected: "F64".to_string(),
337                found: format!("{:?}", data_type),
338            });
339        }
340
341        // Read the data using the mutable reference
342        let data = file.read_var_f64(var_name)?;
343        Ok(data)
344    }
345
346    /// Read i32 data from NetCDF-3 file.
347    #[cfg(feature = "netcdf3")]
348    fn read_i32_nc3(file: &mut netcdf3::FileReader, var_name: &str) -> Result<Vec<i32>> {
349        let dataset = file.data_set();
350        let var_info = dataset
351            .get_var(var_name)
352            .ok_or_else(|| NetCdfError::VariableNotFound {
353                name: var_name.to_string(),
354            })?;
355
356        use netcdf3::DataType as Nc3Type;
357        let data_type = var_info.data_type();
358
359        // Verify data type matches
360        if data_type != Nc3Type::I32 {
361            return Err(NetCdfError::DataTypeMismatch {
362                expected: "I32".to_string(),
363                found: format!("{:?}", data_type),
364            });
365        }
366
367        // Read the data using the mutable reference
368        let data = file.read_var_i32(var_name)?;
369        Ok(data)
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_data_type_conversion() {
379        #[cfg(feature = "netcdf3")]
380        {
381            use netcdf3::DataType as Nc3Type;
382            assert_eq!(
383                NetCdfReader::convert_datatype_nc3(Nc3Type::F32)
384                    .expect("Failed to convert NetCDF datatype in test"),
385                DataType::F32
386            );
387            assert_eq!(
388                NetCdfReader::convert_datatype_nc3(Nc3Type::F64)
389                    .expect("Failed to convert NetCDF datatype in test"),
390                DataType::F64
391            );
392            assert_eq!(
393                NetCdfReader::convert_datatype_nc3(Nc3Type::I32)
394                    .expect("Failed to convert NetCDF datatype in test"),
395                DataType::I32
396            );
397        }
398    }
399}