Skip to main content

oxigdal_netcdf/
writer.rs

1//! NetCDF file writer implementation.
2//!
3//! This module provides functionality for writing NetCDF files, including
4//! creating dimensions, variables, attributes, and writing data.
5
6use std::path::Path;
7
8use crate::attribute::{Attribute, AttributeValue};
9use crate::dimension::Dimension;
10use crate::error::{NetCdfError, Result};
11use crate::metadata::{NetCdfMetadata, NetCdfVersion};
12use crate::variable::{DataType, Variable};
13
14/// Pending variable data to write.
15#[cfg(feature = "netcdf3")]
16enum PendingData {
17    F32(Vec<f32>),
18    F64(Vec<f64>),
19    I32(Vec<i32>),
20    I16(Vec<i16>),
21    I8(Vec<i8>),
22}
23
24/// NetCDF file writer.
25///
26/// Provides methods for creating and writing NetCDF files.
27pub struct NetCdfWriter {
28    metadata: NetCdfMetadata,
29    #[cfg(feature = "netcdf3")]
30    dataset_nc3: Option<netcdf3::DataSet>,
31    #[cfg(feature = "netcdf3")]
32    pending_data: std::collections::HashMap<String, PendingData>,
33    #[cfg(feature = "netcdf4")]
34    file_nc4: Option<netcdf::FileMut>,
35    path: std::path::PathBuf,
36    is_define_mode: bool,
37}
38
39impl std::fmt::Debug for NetCdfWriter {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.debug_struct("NetCdfWriter")
42            .field("path", &self.path)
43            .field("is_define_mode", &self.is_define_mode)
44            .finish_non_exhaustive()
45    }
46}
47
48impl NetCdfWriter {
49    /// Create a new NetCDF file for writing.
50    ///
51    /// # Arguments
52    ///
53    /// * `path` - Path to the NetCDF file
54    /// * `version` - NetCDF format version
55    ///
56    /// # Errors
57    ///
58    /// Returns error if the file cannot be created.
59    #[allow(unused_variables)]
60    pub fn create(path: impl AsRef<Path>, version: NetCdfVersion) -> Result<Self> {
61        let path = path.as_ref();
62
63        if version.is_netcdf4() {
64            #[cfg(feature = "netcdf4")]
65            {
66                Self::create_netcdf4(path)
67            }
68            #[cfg(not(feature = "netcdf4"))]
69            {
70                Err(NetCdfError::NetCdf4NotAvailable)
71            }
72        } else {
73            #[cfg(feature = "netcdf3")]
74            {
75                Self::create_netcdf3(path)
76            }
77            #[cfg(not(feature = "netcdf3"))]
78            {
79                Err(NetCdfError::FeatureNotEnabled {
80                    feature: "netcdf3".to_string(),
81                    message: "Enable 'netcdf3' feature to write NetCDF-3 files".to_string(),
82                })
83            }
84        }
85    }
86
87    /// Create a NetCDF-3 Classic file.
88    ///
89    /// # Errors
90    ///
91    /// Returns error if the file cannot be created.
92    #[cfg(feature = "netcdf3")]
93    pub fn create_netcdf3(path: impl AsRef<Path>) -> Result<Self> {
94        let path = path.as_ref();
95        let dataset = netcdf3::DataSet::new();
96        let metadata = NetCdfMetadata::new_classic();
97
98        Ok(Self {
99            metadata,
100            dataset_nc3: Some(dataset),
101            pending_data: std::collections::HashMap::new(),
102            #[cfg(feature = "netcdf4")]
103            file_nc4: None,
104            path: path.to_path_buf(),
105            is_define_mode: true,
106        })
107    }
108
109    /// Create a NetCDF-4 file.
110    ///
111    /// # Errors
112    ///
113    /// Returns error if the file cannot be created.
114    #[cfg(feature = "netcdf4")]
115    pub fn create_netcdf4(_path: impl AsRef<Path>) -> Result<Self> {
116        // NetCDF-4 support is placeholder for now
117        Err(NetCdfError::NetCdf4NotAvailable)
118    }
119
120    /// Get the file metadata.
121    #[must_use]
122    pub const fn metadata(&self) -> &NetCdfMetadata {
123        &self.metadata
124    }
125
126    /// Add a dimension.
127    ///
128    /// # Errors
129    ///
130    /// Returns error if not in define mode or dimension already exists.
131    pub fn add_dimension(&mut self, dimension: Dimension) -> Result<()> {
132        if !self.is_define_mode {
133            return Err(NetCdfError::Other(
134                "Cannot add dimension outside of define mode".to_string(),
135            ));
136        }
137
138        // Add to metadata
139        self.metadata.dimensions_mut().add(dimension.clone())?;
140
141        // Add to dataset
142        #[cfg(feature = "netcdf3")]
143        if let Some(ref mut dataset) = self.dataset_nc3 {
144            if dimension.is_unlimited() {
145                dataset.set_unlimited_dim(dimension.name(), dimension.len())?;
146            } else {
147                dataset.add_fixed_dim(dimension.name(), dimension.len())?;
148            }
149        }
150
151        Ok(())
152    }
153
154    /// Add a variable.
155    ///
156    /// # Errors
157    ///
158    /// Returns error if not in define mode, variable already exists,
159    /// or variable dimensions don't exist.
160    pub fn add_variable(&mut self, variable: Variable) -> Result<()> {
161        if !self.is_define_mode {
162            return Err(NetCdfError::Other(
163                "Cannot add variable outside of define mode".to_string(),
164            ));
165        }
166
167        // Validate dimensions exist
168        for dim_name in variable.dimension_names() {
169            if !self.metadata.dimensions().contains(dim_name) {
170                return Err(NetCdfError::DimensionNotFound {
171                    name: dim_name.clone(),
172                });
173            }
174        }
175
176        // Add to metadata
177        self.metadata.variables_mut().add(variable.clone())?;
178
179        // Add to dataset
180        #[cfg(feature = "netcdf3")]
181        if let Some(ref mut dataset) = self.dataset_nc3 {
182            let nc3_type = Self::convert_datatype_to_nc3(variable.data_type())?;
183            let dims: Vec<&str> = variable
184                .dimension_names()
185                .iter()
186                .map(|s| s.as_str())
187                .collect();
188            dataset.add_var(variable.name(), &dims, nc3_type)?;
189        }
190
191        Ok(())
192    }
193
194    /// Add a global attribute.
195    ///
196    /// # Errors
197    ///
198    /// Returns error if not in define mode.
199    pub fn add_global_attribute(&mut self, attribute: Attribute) -> Result<()> {
200        if !self.is_define_mode {
201            return Err(NetCdfError::Other(
202                "Cannot add global attribute outside of define mode".to_string(),
203            ));
204        }
205
206        // Add to metadata
207        self.metadata
208            .global_attributes_mut()
209            .add(attribute.clone())?;
210
211        // Add to dataset
212        #[cfg(feature = "netcdf3")]
213        if let Some(ref mut dataset) = self.dataset_nc3 {
214            Self::write_global_attribute_nc3(dataset, &attribute)?;
215        }
216
217        Ok(())
218    }
219
220    /// Add a variable attribute.
221    ///
222    /// # Errors
223    ///
224    /// Returns error if not in define mode or variable doesn't exist.
225    pub fn add_variable_attribute(&mut self, var_name: &str, attribute: Attribute) -> Result<()> {
226        if !self.is_define_mode {
227            return Err(NetCdfError::Other(
228                "Cannot add variable attribute outside of define mode".to_string(),
229            ));
230        }
231
232        // Add to metadata
233        let var = self
234            .metadata
235            .variables_mut()
236            .get_mut(var_name)
237            .ok_or_else(|| NetCdfError::VariableNotFound {
238                name: var_name.to_string(),
239            })?;
240        var.attributes_mut().add(attribute.clone())?;
241
242        // Add to dataset
243        #[cfg(feature = "netcdf3")]
244        if let Some(ref mut dataset) = self.dataset_nc3 {
245            Self::write_variable_attribute_nc3(dataset, var_name, &attribute)?;
246        }
247
248        Ok(())
249    }
250
251    /// End define mode and enter data mode.
252    ///
253    /// After calling this, you can write data but cannot add dimensions,
254    /// variables, or attributes.
255    ///
256    /// # Errors
257    ///
258    /// Returns error if already in data mode or if metadata is invalid.
259    pub fn end_define_mode(&mut self) -> Result<()> {
260        if !self.is_define_mode {
261            return Err(NetCdfError::Other("Already in data mode".to_string()));
262        }
263
264        // Validate metadata
265        self.metadata.validate()?;
266
267        self.is_define_mode = false;
268        Ok(())
269    }
270
271    /// Write f32 data to a variable.
272    ///
273    /// # Errors
274    ///
275    /// Returns error if in define mode, variable doesn't exist,
276    /// or data size doesn't match variable size.
277    pub fn write_f32(&mut self, var_name: &str, data: &[f32]) -> Result<()> {
278        if self.is_define_mode {
279            return Err(NetCdfError::Other(
280                "Cannot write data in define mode. Call end_define_mode() first.".to_string(),
281            ));
282        }
283
284        // Get variable
285        let var = self.metadata.variables().get(var_name).ok_or_else(|| {
286            NetCdfError::VariableNotFound {
287                name: var_name.to_string(),
288            }
289        })?;
290
291        // Validate data size
292        let expected_size = var.size(self.metadata.dimensions())?;
293        if data.len() != expected_size {
294            return Err(NetCdfError::InvalidShape {
295                message: format!(
296                    "Data size {} does not match variable size {}",
297                    data.len(),
298                    expected_size
299                ),
300            });
301        }
302
303        // Store pending data for later write
304        #[cfg(feature = "netcdf3")]
305        {
306            self.pending_data
307                .insert(var_name.to_string(), PendingData::F32(data.to_vec()));
308        }
309
310        Ok(())
311    }
312
313    /// Write f64 data to a variable.
314    ///
315    /// # Errors
316    ///
317    /// Returns error if in define mode, variable doesn't exist,
318    /// or data size doesn't match variable size.
319    pub fn write_f64(&mut self, var_name: &str, data: &[f64]) -> Result<()> {
320        if self.is_define_mode {
321            return Err(NetCdfError::Other(
322                "Cannot write data in define mode. Call end_define_mode() first.".to_string(),
323            ));
324        }
325
326        let var = self.metadata.variables().get(var_name).ok_or_else(|| {
327            NetCdfError::VariableNotFound {
328                name: var_name.to_string(),
329            }
330        })?;
331
332        let expected_size = var.size(self.metadata.dimensions())?;
333        if data.len() != expected_size {
334            return Err(NetCdfError::InvalidShape {
335                message: format!(
336                    "Data size {} does not match variable size {}",
337                    data.len(),
338                    expected_size
339                ),
340            });
341        }
342
343        #[cfg(feature = "netcdf3")]
344        {
345            self.pending_data
346                .insert(var_name.to_string(), PendingData::F64(data.to_vec()));
347        }
348
349        Ok(())
350    }
351
352    /// Write i32 data to a variable.
353    ///
354    /// # Errors
355    ///
356    /// Returns error if in define mode, variable doesn't exist,
357    /// or data size doesn't match variable size.
358    pub fn write_i32(&mut self, var_name: &str, data: &[i32]) -> Result<()> {
359        if self.is_define_mode {
360            return Err(NetCdfError::Other(
361                "Cannot write data in define mode. Call end_define_mode() first.".to_string(),
362            ));
363        }
364
365        let var = self.metadata.variables().get(var_name).ok_or_else(|| {
366            NetCdfError::VariableNotFound {
367                name: var_name.to_string(),
368            }
369        })?;
370
371        let expected_size = var.size(self.metadata.dimensions())?;
372        if data.len() != expected_size {
373            return Err(NetCdfError::InvalidShape {
374                message: format!(
375                    "Data size {} does not match variable size {}",
376                    data.len(),
377                    expected_size
378                ),
379            });
380        }
381
382        #[cfg(feature = "netcdf3")]
383        {
384            self.pending_data
385                .insert(var_name.to_string(), PendingData::I32(data.to_vec()));
386        }
387
388        Ok(())
389    }
390
391    /// Finalize and close the file.
392    ///
393    /// This method consumes the writer and ensures all data is written to disk.
394    ///
395    /// # Errors
396    ///
397    /// Returns error if file cannot be closed.
398    #[cfg(feature = "netcdf3")]
399    pub fn close(self) -> Result<()> {
400        if let Some(dataset) = self.dataset_nc3 {
401            // Remove the file if it already exists (e.g., created by NamedTempFile)
402            if self.path.exists() {
403                std::fs::remove_file(&self.path).map_err(|e| {
404                    NetCdfError::Io(format!("Failed to remove existing file: {}", e))
405                })?;
406            }
407            let mut writer = netcdf3::FileWriter::create_new(&self.path)?;
408            writer.set_def(&dataset, netcdf3::Version::Classic, 0)?;
409
410            // Write all pending data
411            for (var_name, data) in &self.pending_data {
412                match data {
413                    PendingData::F32(values) => {
414                        writer.write_var_f32(var_name, values)?;
415                    }
416                    PendingData::F64(values) => {
417                        writer.write_var_f64(var_name, values)?;
418                    }
419                    PendingData::I32(values) => {
420                        writer.write_var_i32(var_name, values)?;
421                    }
422                    PendingData::I16(values) => {
423                        writer.write_var_i16(var_name, values)?;
424                    }
425                    PendingData::I8(values) => {
426                        writer.write_var_i8(var_name, values)?;
427                    }
428                }
429            }
430
431            writer.close()?;
432        }
433        Ok(())
434    }
435
436    /// Finalize and close the file.
437    #[cfg(not(feature = "netcdf3"))]
438    pub fn close(self) -> Result<()> {
439        Ok(())
440    }
441
442    /// Convert our data type to NetCDF-3 data type.
443    #[cfg(feature = "netcdf3")]
444    fn convert_datatype_to_nc3(dtype: DataType) -> Result<netcdf3::DataType> {
445        use netcdf3::DataType as Nc3Type;
446
447        match dtype {
448            DataType::I8 => Ok(Nc3Type::I8),
449            DataType::I16 => Ok(Nc3Type::I16),
450            DataType::I32 => Ok(Nc3Type::I32),
451            DataType::F32 => Ok(Nc3Type::F32),
452            DataType::F64 => Ok(Nc3Type::F64),
453            DataType::Char => Ok(Nc3Type::U8), // Character data uses U8 in netcdf3 v0.6
454            _ => Err(NetCdfError::DataTypeMismatch {
455                expected: "NetCDF-3 compatible type".to_string(),
456                found: dtype.name().to_string(),
457            }),
458        }
459    }
460
461    /// Write a global attribute to NetCDF-3 dataset.
462    #[cfg(feature = "netcdf3")]
463    fn write_global_attribute_nc3(dataset: &mut netcdf3::DataSet, attr: &Attribute) -> Result<()> {
464        match attr.value() {
465            AttributeValue::Text(s) => {
466                dataset.add_global_attr_string(attr.name(), s)?;
467            }
468            AttributeValue::I8(v) => {
469                dataset.add_global_attr_i8(attr.name(), v.clone())?;
470            }
471            AttributeValue::U8(v) => {
472                dataset.add_global_attr_u8(attr.name(), v.clone())?;
473            }
474            AttributeValue::I16(v) => {
475                dataset.add_global_attr_i16(attr.name(), v.clone())?;
476            }
477            AttributeValue::I32(v) => {
478                dataset.add_global_attr_i32(attr.name(), v.clone())?;
479            }
480            AttributeValue::F32(v) => {
481                dataset.add_global_attr_f32(attr.name(), v.clone())?;
482            }
483            AttributeValue::F64(v) => {
484                dataset.add_global_attr_f64(attr.name(), v.clone())?;
485            }
486            _ => {
487                return Err(NetCdfError::AttributeError(
488                    "Attribute type not supported in NetCDF-3".to_string(),
489                ));
490            }
491        }
492        Ok(())
493    }
494
495    /// Write a variable attribute to NetCDF-3 dataset.
496    #[cfg(feature = "netcdf3")]
497    fn write_variable_attribute_nc3(
498        dataset: &mut netcdf3::DataSet,
499        var_name: &str,
500        attr: &Attribute,
501    ) -> Result<()> {
502        match attr.value() {
503            AttributeValue::Text(s) => {
504                dataset.add_var_attr_string(var_name, attr.name(), s)?;
505            }
506            AttributeValue::I8(v) => {
507                dataset.add_var_attr_i8(var_name, attr.name(), v.clone())?;
508            }
509            AttributeValue::U8(v) => {
510                dataset.add_var_attr_u8(var_name, attr.name(), v.clone())?;
511            }
512            AttributeValue::I16(v) => {
513                dataset.add_var_attr_i16(var_name, attr.name(), v.clone())?;
514            }
515            AttributeValue::I32(v) => {
516                dataset.add_var_attr_i32(var_name, attr.name(), v.clone())?;
517            }
518            AttributeValue::F32(v) => {
519                dataset.add_var_attr_f32(var_name, attr.name(), v.clone())?;
520            }
521            AttributeValue::F64(v) => {
522                dataset.add_var_attr_f64(var_name, attr.name(), v.clone())?;
523            }
524            _ => {
525                return Err(NetCdfError::AttributeError(
526                    "Attribute type not supported in NetCDF-3".to_string(),
527                ));
528            }
529        }
530        Ok(())
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537
538    #[test]
539    fn test_data_type_conversion() {
540        #[cfg(feature = "netcdf3")]
541        {
542            use netcdf3::DataType as Nc3Type;
543            assert_eq!(
544                NetCdfWriter::convert_datatype_to_nc3(DataType::F32).expect("F32 conversion"),
545                Nc3Type::F32
546            );
547            assert_eq!(
548                NetCdfWriter::convert_datatype_to_nc3(DataType::F64).expect("F64 conversion"),
549                Nc3Type::F64
550            );
551            assert_eq!(
552                NetCdfWriter::convert_datatype_to_nc3(DataType::I32).expect("I32 conversion"),
553                Nc3Type::I32
554            );
555
556            // U16 is not supported in NetCDF-3
557            assert!(NetCdfWriter::convert_datatype_to_nc3(DataType::U16).is_err());
558        }
559    }
560}