Skip to main content

netcdf_reader/classic/
variable.rs

1//! Variable-level read methods for classic NetCDF files.
2//!
3//! Provides convenience methods for reading variable data from a `ClassicFile`,
4//! with type-checked access and support for both record and non-record variables.
5
6use ndarray::ArrayD;
7
8use crate::error::{Error, Result};
9use crate::types::{NcType, NcVariable};
10
11use super::data::{self, compute_record_stride, NcReadType};
12use super::ClassicFile;
13
14impl ClassicFile {
15    /// Read a variable's data as an ndarray of the specified type.
16    ///
17    /// The type parameter `T` must match the variable's NetCDF type. For example,
18    /// use `f32` for NC_FLOAT variables and `f64` for NC_DOUBLE variables.
19    pub fn read_variable<T: NcReadType>(&self, name: &str) -> Result<ArrayD<T>> {
20        let var = self.find_variable(name)?;
21
22        // Check type compatibility.
23        let expected = T::nc_type();
24        if var.dtype != expected {
25            return Err(Error::TypeMismatch {
26                expected: format!("{:?}", expected),
27                actual: format!("{:?}", var.dtype),
28            });
29        }
30
31        let file_data = self.data.as_slice();
32
33        if var.is_record_var {
34            let record_stride = compute_record_stride(&self.root_group.variables);
35            data::read_record_variable(file_data, var, self.numrecs, record_stride)
36        } else {
37            data::read_non_record_variable(file_data, var)
38        }
39    }
40
41    /// Read a variable's data with automatic type promotion to f64.
42    ///
43    /// This reads any numeric variable and converts all values to f64,
44    /// which is convenient for analysis but may lose precision for i64/u64.
45    pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
46        let var = self.find_variable(name)?;
47        let file_data = self.data.as_slice();
48
49        match var.dtype {
50            NcType::Byte => {
51                let arr = self.read_typed_variable::<i8>(var, file_data)?;
52                Ok(arr.mapv(|v| v as f64))
53            }
54            NcType::Short => {
55                let arr = self.read_typed_variable::<i16>(var, file_data)?;
56                Ok(arr.mapv(|v| v as f64))
57            }
58            NcType::Int => {
59                let arr = self.read_typed_variable::<i32>(var, file_data)?;
60                Ok(arr.mapv(|v| v as f64))
61            }
62            NcType::Float => {
63                let arr = self.read_typed_variable::<f32>(var, file_data)?;
64                Ok(arr.mapv(|v| v as f64))
65            }
66            NcType::Double => self.read_typed_variable::<f64>(var, file_data),
67            NcType::UByte => {
68                let arr = self.read_typed_variable::<u8>(var, file_data)?;
69                Ok(arr.mapv(|v| v as f64))
70            }
71            NcType::UShort => {
72                let arr = self.read_typed_variable::<u16>(var, file_data)?;
73                Ok(arr.mapv(|v| v as f64))
74            }
75            NcType::UInt => {
76                let arr = self.read_typed_variable::<u32>(var, file_data)?;
77                Ok(arr.mapv(|v| v as f64))
78            }
79            NcType::Int64 => {
80                let arr = self.read_typed_variable::<i64>(var, file_data)?;
81                Ok(arr.mapv(|v| v as f64))
82            }
83            NcType::UInt64 => {
84                let arr = self.read_typed_variable::<u64>(var, file_data)?;
85                Ok(arr.mapv(|v| v as f64))
86            }
87            NcType::Char => Err(Error::TypeMismatch {
88                expected: "numeric type".to_string(),
89                actual: "Char".to_string(),
90            }),
91            NcType::String => Err(Error::TypeMismatch {
92                expected: "numeric type".to_string(),
93                actual: "String".to_string(),
94            }),
95            _ => Err(Error::TypeMismatch {
96                expected: "numeric type".to_string(),
97                actual: format!("{:?}", var.dtype),
98            }),
99        }
100    }
101
102    /// Read a char variable as a String (or Vec<String> for multi-dimensional).
103    pub fn read_variable_as_string(&self, name: &str) -> Result<String> {
104        let var = self.find_variable(name)?;
105        if var.dtype != NcType::Char {
106            return Err(Error::TypeMismatch {
107                expected: "Char".to_string(),
108                actual: format!("{:?}", var.dtype),
109            });
110        }
111
112        let file_data = self.data.as_slice();
113        let arr = self.read_typed_variable::<u8>(var, file_data)?;
114        let bytes: Vec<u8> = arr.iter().copied().collect();
115        let s = String::from_utf8_lossy(&bytes)
116            .trim_end_matches('\0')
117            .to_string();
118        Ok(s)
119    }
120
121    /// Read a slice (hyperslab) of a variable.
122    ///
123    /// For non-record variables where the selection only restricts the outermost
124    /// dimension (inner dims select full range, step=1), byte offsets are computed
125    /// directly to avoid reading the entire variable. Otherwise falls back to
126    /// full-read-then-slice.
127    pub fn read_variable_slice<T: NcReadType>(
128        &self,
129        name: &str,
130        selection: &crate::types::NcSliceInfo,
131    ) -> Result<ArrayD<T>> {
132        use crate::types::NcSliceInfoElem;
133        use ndarray::IxDyn;
134
135        let var = self.find_variable(name)?;
136        let expected = T::nc_type();
137        if var.dtype != expected {
138            return Err(Error::TypeMismatch {
139                expected: format!("{:?}", expected),
140                actual: format!("{:?}", var.dtype),
141            });
142        }
143        let file_data = self.data.as_slice();
144
145        // For non-record variables, try direct byte-offset extraction.
146        if !var.is_record_var && var.ndim() > 0 {
147            let shape: Vec<u64> = var.shape();
148            let ndim = shape.len();
149
150            // Check if inner dims are all full-range with step=1.
151            let can_direct = selection.selections.len() == ndim
152                && selection
153                    .selections
154                    .iter()
155                    .enumerate()
156                    .skip(1)
157                    .all(|(d, sel)| {
158                        matches!(sel, NcSliceInfoElem::Slice { start, end, step }
159                        if *start == 0 && *step == 1 && (*end == u64::MAX || *end >= shape[d]))
160                    });
161
162            if can_direct {
163                let elem_size = T::element_size();
164                let row_elements = crate::types::checked_shape_elements(
165                    &shape[1..],
166                    "classic slice row element count",
167                )?
168                .max(1);
169                let row_bytes = crate::types::checked_usize_from_u64(
170                    crate::types::checked_mul_u64(
171                        row_elements,
172                        elem_size as u64,
173                        "classic slice row size in bytes",
174                    )?,
175                    "classic slice row size in bytes",
176                )?;
177
178                let (first_row, num_rows, result_shape) = match &selection.selections[0] {
179                    NcSliceInfoElem::Index(idx) => {
180                        let rs: Vec<usize> = shape[1..]
181                            .iter()
182                            .map(|&d| {
183                                crate::types::checked_usize_from_u64(
184                                    d,
185                                    "classic slice result dimension",
186                                )
187                            })
188                            .collect::<Result<Vec<_>>>()?;
189                        (*idx, 1u64, rs)
190                    }
191                    NcSliceInfoElem::Slice { start, end, step } => {
192                        let actual_end = if *end == u64::MAX {
193                            shape[0]
194                        } else {
195                            (*end).min(shape[0])
196                        };
197                        let count = (actual_end - start).div_ceil(*step);
198                        if *step == 1 {
199                            let mut rs = vec![crate::types::checked_usize_from_u64(
200                                count,
201                                "classic slice result dimension",
202                            )?];
203                            rs.extend(
204                                shape[1..]
205                                    .iter()
206                                    .map(|&d| {
207                                        crate::types::checked_usize_from_u64(
208                                            d,
209                                            "classic slice result dimension",
210                                        )
211                                    })
212                                    .collect::<Result<Vec<_>>>()?,
213                            );
214                            (*start, count, rs)
215                        } else {
216                            // Step > 1: can't do contiguous read, fall through.
217                            return {
218                                let full = data::read_non_record_variable(file_data, var)?;
219                                slice_classic_array(&full, var, selection, 0)
220                            };
221                        }
222                    }
223                };
224
225                let byte_offset = crate::types::checked_usize_from_u64(
226                    var.data_offset,
227                    "classic slice data offset",
228                )?
229                .checked_add(
230                    crate::types::checked_usize_from_u64(first_row, "classic slice row offset")?
231                        .checked_mul(row_bytes)
232                        .ok_or_else(|| {
233                            Error::InvalidData(
234                                "classic slice byte offset exceeds platform usize".to_string(),
235                            )
236                        })?,
237                )
238                .ok_or_else(|| {
239                    Error::InvalidData(
240                        "classic slice byte offset exceeds platform usize".to_string(),
241                    )
242                })?;
243                let total_bytes =
244                    crate::types::checked_usize_from_u64(num_rows, "classic slice row count")?
245                        .checked_mul(row_bytes)
246                        .ok_or_else(|| {
247                            Error::InvalidData(
248                                "classic slice byte count exceeds platform usize".to_string(),
249                            )
250                        })?;
251                let total_elements = crate::types::checked_usize_from_u64(
252                    crate::types::checked_mul_u64(
253                        num_rows,
254                        row_elements,
255                        "classic slice element count",
256                    )?,
257                    "classic slice element count",
258                )?;
259
260                let end = byte_offset.checked_add(total_bytes).ok_or_else(|| {
261                    Error::InvalidData(
262                        "classic slice byte range exceeds platform usize".to_string(),
263                    )
264                })?;
265                if end > file_data.len() {
266                    return Err(Error::InvalidData(format!(
267                        "variable '{}' slice data extends beyond file",
268                        var.name
269                    )));
270                }
271
272                let data_slice = &file_data[byte_offset..end];
273                let values = T::decode_bulk_be(data_slice, total_elements)?;
274
275                return ndarray::ArrayD::from_shape_vec(IxDyn(&result_shape), values)
276                    .map_err(|e| Error::InvalidData(format!("failed to create array: {}", e)));
277            }
278        }
279
280        // Fallback: read full variable then slice.
281        if var.is_record_var {
282            let record_stride = compute_record_stride(&self.root_group.variables);
283            let full = data::read_record_variable(file_data, var, self.numrecs, record_stride)?;
284            slice_classic_array(&full, var, selection, self.numrecs)
285        } else {
286            let full = data::read_non_record_variable(file_data, var)?;
287            slice_classic_array(&full, var, selection, 0)
288        }
289    }
290
291    /// Read a slice with automatic type promotion to f64.
292    pub fn read_variable_slice_as_f64(
293        &self,
294        name: &str,
295        selection: &crate::types::NcSliceInfo,
296    ) -> Result<ArrayD<f64>> {
297        let var = self.find_variable(name)?;
298        let file_data = self.data.as_slice();
299
300        macro_rules! slice_promoted {
301            ($ty:ty) => {{
302                let full = self.read_typed_variable::<$ty>(var, file_data)?;
303                let full_f64 = full.mapv(|v| v as f64);
304                slice_classic_array(
305                    &full_f64,
306                    var,
307                    selection,
308                    if var.is_record_var { self.numrecs } else { 0 },
309                )
310            }};
311        }
312
313        match var.dtype {
314            NcType::Byte => slice_promoted!(i8),
315            NcType::Short => slice_promoted!(i16),
316            NcType::Int => slice_promoted!(i32),
317            NcType::Float => slice_promoted!(f32),
318            NcType::Double => slice_promoted!(f64),
319            NcType::UByte => slice_promoted!(u8),
320            NcType::UShort => slice_promoted!(u16),
321            NcType::UInt => slice_promoted!(u32),
322            NcType::Int64 => slice_promoted!(i64),
323            NcType::UInt64 => slice_promoted!(u64),
324            NcType::Char => Err(Error::TypeMismatch {
325                expected: "numeric type".to_string(),
326                actual: "Char".to_string(),
327            }),
328            _ => Err(Error::TypeMismatch {
329                expected: "numeric type".to_string(),
330                actual: format!("{:?}", var.dtype),
331            }),
332        }
333    }
334
335    /// Internal: find a variable by name.
336    fn find_variable(&self, name: &str) -> Result<&NcVariable> {
337        self.root_group
338            .variables
339            .iter()
340            .find(|v| v.name == name)
341            .ok_or_else(|| Error::VariableNotFound(name.to_string()))
342    }
343
344    /// Internal: read a variable with the correct record handling.
345    fn read_typed_variable<T: NcReadType>(
346        &self,
347        var: &NcVariable,
348        file_data: &[u8],
349    ) -> Result<ArrayD<T>> {
350        if var.is_record_var {
351            let record_stride = compute_record_stride(&self.root_group.variables);
352            data::read_record_variable(file_data, var, self.numrecs, record_stride)
353        } else {
354            data::read_non_record_variable(file_data, var)
355        }
356    }
357}
358
359/// Apply a NcSliceInfo selection to an already-read classic array.
360///
361/// For classic format, we read the full variable first then extract the
362/// hyperslab, since the data is contiguous (non-record) or interleaved (record).
363fn slice_classic_array<T: Clone + Default + 'static>(
364    full: &ArrayD<T>,
365    var: &NcVariable,
366    selection: &crate::types::NcSliceInfo,
367    numrecs: u64,
368) -> Result<ArrayD<T>> {
369    use crate::types::NcSliceInfoElem;
370    use ndarray::Slice;
371
372    let ndim = var.ndim();
373    if selection.selections.len() != ndim {
374        return Err(Error::InvalidData(format!(
375            "selection has {} dimensions but variable '{}' has {}",
376            selection.selections.len(),
377            var.name,
378            ndim
379        )));
380    }
381
382    // Build the shape of the full array, resolving unlimited dims.
383    let mut shape: Vec<usize> = var.shape().iter().map(|&s| s as usize).collect();
384    if var.is_record_var && !shape.is_empty() {
385        shape[0] = numrecs as usize;
386    }
387
388    // First, apply range slicing on all dimensions.
389    let mut view = full.view();
390    for (d, sel) in selection.selections.iter().enumerate() {
391        let dim_size = shape[d] as u64;
392        match sel {
393            NcSliceInfoElem::Index(idx) => {
394                if *idx >= dim_size {
395                    return Err(Error::InvalidData(format!(
396                        "index {} out of bounds for dimension {} (size {})",
397                        idx, d, dim_size
398                    )));
399                }
400                // Slice to a single element (will collapse later).
401                view.slice_axis_inplace(
402                    ndarray::Axis(d),
403                    Slice::new(*idx as isize, Some(*idx as isize + 1), 1),
404                );
405            }
406            NcSliceInfoElem::Slice { start, end, step } => {
407                let actual_end = if *end == u64::MAX {
408                    dim_size as isize
409                } else {
410                    (*end).min(dim_size) as isize
411                };
412                view.slice_axis_inplace(
413                    ndarray::Axis(d),
414                    Slice::new(*start as isize, Some(actual_end), *step as isize),
415                );
416            }
417        }
418    }
419
420    // Now collapse Index dimensions (remove axes of size 1 from Index selections).
421    let mut result = view.to_owned();
422    let mut removed = 0;
423    for (d, sel) in selection.selections.iter().enumerate() {
424        if matches!(sel, NcSliceInfoElem::Index(_)) {
425            let axis = d - removed;
426            result = result.index_axis_move(ndarray::Axis(axis), 0);
427            removed += 1;
428        }
429    }
430
431    Ok(result)
432}