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::{
10    checked_mul_u64, checked_shape_elements, checked_usize_from_u64, NcType, NcVariable,
11};
12
13use super::data::{self, compute_record_stride, NcReadType};
14use super::ClassicFile;
15
16#[derive(Clone, Debug)]
17enum ResolvedClassicSelectionDim {
18    Index(u64),
19    Slice {
20        start: u64,
21        step: u64,
22        count: usize,
23        is_full_unit_stride: bool,
24    },
25}
26
27impl ResolvedClassicSelectionDim {
28    fn is_full_unit_stride(&self) -> bool {
29        matches!(
30            self,
31            Self::Slice {
32                is_full_unit_stride: true,
33                ..
34            }
35        )
36    }
37}
38
39#[derive(Clone, Debug)]
40struct ResolvedClassicSelection {
41    dims: Vec<ResolvedClassicSelectionDim>,
42    result_shape: Vec<usize>,
43    result_elements: usize,
44}
45
46struct BlockReadContext<'a> {
47    file_data: &'a [u8],
48    var_name: &'a str,
49    base_offset: usize,
50    plan: &'a ContiguousSelectionPlan,
51}
52
53struct ContiguousSelectionPlan {
54    dims: Vec<ResolvedClassicSelectionDim>,
55    strides: Vec<u64>,
56    tail_start: usize,
57    block_elements: usize,
58    block_bytes: usize,
59    elem_size: u64,
60}
61
62struct RecordSliceContext<'a> {
63    file_data: &'a [u8],
64    var_name: &'a str,
65    base_offset: usize,
66    record_stride: usize,
67    inner_resolved: &'a ResolvedClassicSelection,
68    inner_plan: &'a ContiguousSelectionPlan,
69}
70
71impl ClassicFile {
72    /// Read a variable's data as an ndarray of the specified type.
73    ///
74    /// The type parameter `T` must match the variable's NetCDF type. For example,
75    /// use `f32` for NC_FLOAT variables and `f64` for NC_DOUBLE variables.
76    pub fn read_variable<T: NcReadType>(&self, name: &str) -> Result<ArrayD<T>> {
77        let var = self.find_variable(name)?;
78
79        // Check type compatibility.
80        let expected = T::nc_type();
81        if var.dtype != expected {
82            return Err(Error::TypeMismatch {
83                expected: format!("{:?}", expected),
84                actual: format!("{:?}", var.dtype),
85            });
86        }
87
88        let file_data = self.data.as_slice();
89
90        if var.is_record_var {
91            let record_stride = compute_record_stride(&self.root_group.variables);
92            data::read_record_variable(file_data, var, self.numrecs, record_stride)
93        } else {
94            data::read_non_record_variable(file_data, var)
95        }
96    }
97
98    /// Read a variable's data into a caller-provided typed buffer.
99    pub fn read_variable_into<T: NcReadType>(&self, name: &str, dst: &mut [T]) -> Result<()> {
100        let var = self.find_variable(name)?;
101
102        let expected = T::nc_type();
103        if var.dtype != expected {
104            return Err(Error::TypeMismatch {
105                expected: format!("{:?}", expected),
106                actual: format!("{:?}", var.dtype),
107            });
108        }
109
110        let file_data = self.data.as_slice();
111
112        if var.is_record_var {
113            let record_stride = compute_record_stride(&self.root_group.variables);
114            data::read_record_variable_into(file_data, var, self.numrecs, record_stride, dst)
115        } else {
116            data::read_non_record_variable_into(file_data, var, dst)
117        }
118    }
119
120    /// Read a variable's data with automatic type promotion to f64.
121    ///
122    /// This reads any numeric variable and converts all values to f64,
123    /// which is convenient for analysis but may lose precision for i64/u64.
124    pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
125        let var = self.find_variable(name)?;
126        let file_data = self.data.as_slice();
127
128        match var.dtype {
129            NcType::Byte => {
130                let arr = self.read_typed_variable::<i8>(var, file_data)?;
131                Ok(arr.mapv(|v| v as f64))
132            }
133            NcType::Short => {
134                let arr = self.read_typed_variable::<i16>(var, file_data)?;
135                Ok(arr.mapv(|v| v as f64))
136            }
137            NcType::Int => {
138                let arr = self.read_typed_variable::<i32>(var, file_data)?;
139                Ok(arr.mapv(|v| v as f64))
140            }
141            NcType::Float => {
142                let arr = self.read_typed_variable::<f32>(var, file_data)?;
143                Ok(arr.mapv(|v| v as f64))
144            }
145            NcType::Double => self.read_typed_variable::<f64>(var, file_data),
146            NcType::UByte => {
147                let arr = self.read_typed_variable::<u8>(var, file_data)?;
148                Ok(arr.mapv(|v| v as f64))
149            }
150            NcType::UShort => {
151                let arr = self.read_typed_variable::<u16>(var, file_data)?;
152                Ok(arr.mapv(|v| v as f64))
153            }
154            NcType::UInt => {
155                let arr = self.read_typed_variable::<u32>(var, file_data)?;
156                Ok(arr.mapv(|v| v as f64))
157            }
158            NcType::Int64 => {
159                let arr = self.read_typed_variable::<i64>(var, file_data)?;
160                Ok(arr.mapv(|v| v as f64))
161            }
162            NcType::UInt64 => {
163                let arr = self.read_typed_variable::<u64>(var, file_data)?;
164                Ok(arr.mapv(|v| v as f64))
165            }
166            NcType::Char => Err(Error::TypeMismatch {
167                expected: "numeric type".to_string(),
168                actual: "Char".to_string(),
169            }),
170            NcType::String => Err(Error::TypeMismatch {
171                expected: "numeric type".to_string(),
172                actual: "String".to_string(),
173            }),
174            _ => Err(Error::TypeMismatch {
175                expected: "numeric type".to_string(),
176                actual: format!("{:?}", var.dtype),
177            }),
178        }
179    }
180
181    /// Read a char variable as a String (or Vec<String> for multi-dimensional).
182    pub fn read_variable_as_string(&self, name: &str) -> Result<String> {
183        let mut strings = self.read_variable_as_strings(name)?;
184        match strings.len() {
185            1 => Ok(strings.swap_remove(0)),
186            0 => Err(Error::InvalidData(format!(
187                "variable '{}' contains no string elements",
188                name
189            ))),
190            count => Err(Error::InvalidData(format!(
191                "variable '{}' contains {count} strings; use read_variable_as_strings()",
192                name
193            ))),
194        }
195    }
196
197    /// Read a char variable as a flat vector of strings.
198    ///
199    /// For 2-D and higher char arrays, the last dimension is interpreted as the
200    /// string length and the leading dimensions are flattened.
201    pub fn read_variable_as_strings(&self, name: &str) -> Result<Vec<String>> {
202        let var = self.find_variable(name)?;
203        if var.dtype != NcType::Char {
204            return Err(Error::TypeMismatch {
205                expected: "Char".to_string(),
206                actual: format!("{:?}", var.dtype),
207            });
208        }
209
210        let file_data = self.data.as_slice();
211        let arr = self.read_typed_variable::<u8>(var, file_data)?;
212        let bytes: Vec<u8> = arr.iter().copied().collect();
213        decode_char_variable_strings(var, &bytes)
214    }
215
216    /// Read a slice (hyperslab) of a variable.
217    ///
218    /// Classic variables are read directly from the on-disk byte ranges for
219    /// arbitrary selections.
220    pub fn read_variable_slice<T: NcReadType>(
221        &self,
222        name: &str,
223        selection: &crate::types::NcSliceInfo,
224    ) -> Result<ArrayD<T>> {
225        let var = self.find_variable(name)?;
226        let expected = T::nc_type();
227        if var.dtype != expected {
228            return Err(Error::TypeMismatch {
229                expected: format!("{:?}", expected),
230                actual: format!("{:?}", var.dtype),
231            });
232        }
233        let file_data = self.data.as_slice();
234        let resolved = resolve_classic_selection(
235            var,
236            selection,
237            if var.is_record_var { self.numrecs } else { 0 },
238        )?;
239
240        if !var.is_record_var {
241            return read_non_record_variable_slice_direct(file_data, var, &resolved);
242        }
243
244        let record_stride = compute_record_stride(&self.root_group.variables);
245        read_record_variable_slice_direct(file_data, var, self.numrecs, record_stride, &resolved)
246    }
247
248    /// Read a slice with automatic type promotion to f64.
249    pub fn read_variable_slice_as_f64(
250        &self,
251        name: &str,
252        selection: &crate::types::NcSliceInfo,
253    ) -> Result<ArrayD<f64>> {
254        let var = self.find_variable(name)?;
255
256        macro_rules! slice_promoted {
257            ($ty:ty) => {{
258                let sliced = self.read_variable_slice::<$ty>(name, selection)?;
259                Ok(sliced.mapv(|v| v as f64))
260            }};
261        }
262
263        match var.dtype {
264            NcType::Byte => slice_promoted!(i8),
265            NcType::Short => slice_promoted!(i16),
266            NcType::Int => slice_promoted!(i32),
267            NcType::Float => slice_promoted!(f32),
268            NcType::Double => slice_promoted!(f64),
269            NcType::UByte => slice_promoted!(u8),
270            NcType::UShort => slice_promoted!(u16),
271            NcType::UInt => slice_promoted!(u32),
272            NcType::Int64 => slice_promoted!(i64),
273            NcType::UInt64 => slice_promoted!(u64),
274            NcType::Char => Err(Error::TypeMismatch {
275                expected: "numeric type".to_string(),
276                actual: "Char".to_string(),
277            }),
278            _ => Err(Error::TypeMismatch {
279                expected: "numeric type".to_string(),
280                actual: format!("{:?}", var.dtype),
281            }),
282        }
283    }
284
285    /// Internal: find a variable by name.
286    fn find_variable(&self, name: &str) -> Result<&NcVariable> {
287        self.root_group
288            .variables
289            .iter()
290            .find(|v| v.name == name)
291            .ok_or_else(|| Error::VariableNotFound(name.to_string()))
292    }
293
294    /// Internal: read a variable with the correct record handling.
295    fn read_typed_variable<T: NcReadType>(
296        &self,
297        var: &NcVariable,
298        file_data: &[u8],
299    ) -> Result<ArrayD<T>> {
300        if var.is_record_var {
301            let record_stride = compute_record_stride(&self.root_group.variables);
302            data::read_record_variable(file_data, var, self.numrecs, record_stride)
303        } else {
304            data::read_non_record_variable(file_data, var)
305        }
306    }
307}
308
309fn decode_char_variable_strings(var: &NcVariable, bytes: &[u8]) -> Result<Vec<String>> {
310    let shape = var.shape();
311    if shape.len() <= 1 {
312        return Ok(vec![decode_char_string(bytes)]);
313    }
314
315    let string_len = checked_usize_from_u64(
316        *shape
317            .last()
318            .ok_or_else(|| Error::InvalidData("char variable missing string axis".into()))?,
319        "char string length",
320    )?;
321    let string_count_u64 = checked_shape_elements(&shape[..shape.len() - 1], "char string count")?;
322    let string_count = checked_usize_from_u64(string_count_u64, "char string count")?;
323    let expected_bytes = string_count.checked_mul(string_len).ok_or_else(|| {
324        Error::InvalidData("char string byte count exceeds platform usize".to_string())
325    })?;
326
327    if bytes.len() < expected_bytes {
328        return Err(Error::InvalidData(format!(
329            "char variable '{}' data too short: need {} bytes, have {}",
330            var.name,
331            expected_bytes,
332            bytes.len()
333        )));
334    }
335
336    if string_len == 0 {
337        return Ok(vec![String::new(); string_count]);
338    }
339
340    Ok(bytes[..expected_bytes]
341        .chunks_exact(string_len)
342        .map(decode_char_string)
343        .collect())
344}
345
346fn decode_char_string(bytes: &[u8]) -> String {
347    String::from_utf8_lossy(bytes)
348        .trim_end_matches('\0')
349        .to_string()
350}
351
352fn variable_shape_for_selection(var: &NcVariable, numrecs: u64) -> Vec<u64> {
353    let mut shape = var.shape();
354    if var.is_record_var && !shape.is_empty() {
355        shape[0] = numrecs;
356    }
357    shape
358}
359
360fn row_major_strides(shape: &[u64], context: &str) -> Result<Vec<u64>> {
361    let ndim = shape.len();
362    if ndim == 0 {
363        return Ok(Vec::new());
364    }
365
366    let mut strides = vec![1u64; ndim];
367    for i in (0..ndim - 1).rev() {
368        strides[i] = checked_mul_u64(strides[i + 1], shape[i + 1], context)?;
369    }
370    Ok(strides)
371}
372
373fn resolve_classic_selection(
374    var: &NcVariable,
375    selection: &crate::types::NcSliceInfo,
376    numrecs: u64,
377) -> Result<ResolvedClassicSelection> {
378    use crate::types::NcSliceInfoElem;
379
380    let shape = variable_shape_for_selection(var, numrecs);
381    if selection.selections.len() != shape.len() {
382        return Err(Error::InvalidData(format!(
383            "selection has {} dimensions but variable '{}' has {}",
384            selection.selections.len(),
385            var.name,
386            shape.len()
387        )));
388    }
389
390    let mut dims = Vec::with_capacity(shape.len());
391    let mut result_shape = Vec::new();
392    let mut result_elements = 1usize;
393
394    for (dim, (sel, &dim_size)) in selection.selections.iter().zip(shape.iter()).enumerate() {
395        match sel {
396            NcSliceInfoElem::Index(idx) => {
397                if *idx >= dim_size {
398                    return Err(Error::InvalidData(format!(
399                        "index {} out of bounds for dimension {} (size {})",
400                        idx, dim, dim_size
401                    )));
402                }
403                dims.push(ResolvedClassicSelectionDim::Index(*idx));
404            }
405            NcSliceInfoElem::Slice { start, end, step } => {
406                if *step == 0 {
407                    return Err(Error::InvalidData("slice step cannot be 0".to_string()));
408                }
409                if *start > dim_size {
410                    return Err(Error::InvalidData(format!(
411                        "slice start {} out of bounds for dimension {} (size {})",
412                        start, dim, dim_size
413                    )));
414                }
415
416                let actual_end = if *end == u64::MAX {
417                    dim_size
418                } else {
419                    (*end).min(dim_size)
420                };
421                let count_u64 = if *start >= actual_end {
422                    0
423                } else {
424                    (actual_end - *start).div_ceil(*step)
425                };
426                let count = checked_usize_from_u64(count_u64, "classic slice result dimension")?;
427
428                result_shape.push(count);
429                result_elements = result_elements.checked_mul(count).ok_or_else(|| {
430                    Error::InvalidData(
431                        "classic slice result element count exceeds platform usize".to_string(),
432                    )
433                })?;
434                dims.push(ResolvedClassicSelectionDim::Slice {
435                    start: *start,
436                    step: *step,
437                    count,
438                    is_full_unit_stride: *start == 0 && actual_end == dim_size && *step == 1,
439                });
440            }
441        }
442    }
443
444    Ok(ResolvedClassicSelection {
445        dims,
446        result_shape,
447        result_elements,
448    })
449}
450
451fn read_non_record_variable_slice_direct<T: NcReadType>(
452    file_data: &[u8],
453    var: &NcVariable,
454    resolved: &ResolvedClassicSelection,
455) -> Result<ArrayD<T>> {
456    let shape = variable_shape_for_selection(var, 0);
457    let base_offset = checked_usize_from_u64(var.data_offset, "classic slice data offset")?;
458    build_array_from_contiguous_selection::<T>(file_data, &var.name, base_offset, &shape, resolved)
459}
460
461fn read_record_variable_slice_direct<T: NcReadType>(
462    file_data: &[u8],
463    var: &NcVariable,
464    numrecs: u64,
465    record_stride: u64,
466    resolved: &ResolvedClassicSelection,
467) -> Result<ArrayD<T>> {
468    use ndarray::IxDyn;
469
470    if resolved.result_elements == 0 {
471        return ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), Vec::new())
472            .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")));
473    }
474
475    let shape = variable_shape_for_selection(var, numrecs);
476    let inner_shape = &shape[1..];
477    let inner_dims = resolved.dims[1..].to_vec();
478    let inner_resolved = ResolvedClassicSelection {
479        result_shape: selection_result_shape(&inner_dims),
480        result_elements: selection_result_elements(&inner_dims)?,
481        dims: inner_dims,
482    };
483    let inner_plan = build_contiguous_selection_plan::<T>(inner_shape, &inner_resolved.dims)?;
484    let base_offset = checked_usize_from_u64(var.data_offset, "classic slice data offset")?;
485    let record_stride = checked_usize_from_u64(record_stride, "classic record stride")?;
486    let mut values = Vec::with_capacity(resolved.result_elements);
487    let context = RecordSliceContext {
488        file_data,
489        var_name: &var.name,
490        base_offset,
491        record_stride,
492        inner_resolved: &inner_resolved,
493        inner_plan: &inner_plan,
494    };
495
496    match &resolved.dims[0] {
497        ResolvedClassicSelectionDim::Index(record) => {
498            append_one_record_slice::<T>(&context, *record, &mut values)?
499        }
500        ResolvedClassicSelectionDim::Slice {
501            start, step, count, ..
502        } => {
503            for ordinal in 0..*count {
504                let record = start
505                    .checked_add(checked_mul_u64(
506                        ordinal as u64,
507                        *step,
508                        "classic record slice coordinate",
509                    )?)
510                    .ok_or_else(|| {
511                        Error::InvalidData(
512                            "classic record slice coordinate exceeds u64".to_string(),
513                        )
514                    })?;
515                append_one_record_slice::<T>(&context, record, &mut values)?;
516            }
517        }
518    }
519
520    debug_assert_eq!(values.len(), resolved.result_elements);
521    ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
522        .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
523}
524
525fn selection_result_shape(dims: &[ResolvedClassicSelectionDim]) -> Vec<usize> {
526    dims.iter()
527        .filter_map(|dim| match dim {
528            ResolvedClassicSelectionDim::Index(_) => None,
529            ResolvedClassicSelectionDim::Slice { count, .. } => Some(*count),
530        })
531        .collect()
532}
533
534fn selection_result_elements(dims: &[ResolvedClassicSelectionDim]) -> Result<usize> {
535    let mut elements = 1usize;
536    for dim in dims {
537        if let ResolvedClassicSelectionDim::Slice { count, .. } = dim {
538            elements = elements.checked_mul(*count).ok_or_else(|| {
539                Error::InvalidData(
540                    "classic slice result element count exceeds platform usize".to_string(),
541                )
542            })?;
543        }
544    }
545    Ok(elements)
546}
547
548fn build_array_from_contiguous_selection<T: NcReadType>(
549    file_data: &[u8],
550    var_name: &str,
551    base_offset: usize,
552    shape: &[u64],
553    resolved: &ResolvedClassicSelection,
554) -> Result<ArrayD<T>> {
555    use ndarray::IxDyn;
556
557    let plan = build_contiguous_selection_plan::<T>(shape, &resolved.dims)?;
558    let values = read_contiguous_selection_values_with_plan::<T>(
559        file_data,
560        var_name,
561        base_offset,
562        &plan,
563        resolved.result_elements,
564    )?;
565    ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
566        .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
567}
568
569fn build_contiguous_selection_plan<T: NcReadType>(
570    shape: &[u64],
571    dims: &[ResolvedClassicSelectionDim],
572) -> Result<ContiguousSelectionPlan> {
573    let strides = row_major_strides(shape, "classic slice stride")?;
574    let tail_start = dims
575        .iter()
576        .rposition(|dim| !dim.is_full_unit_stride())
577        .map_or(0, |idx| idx + 1);
578    let block_elements_u64 = checked_shape_elements(
579        &shape[tail_start..],
580        "classic slice contiguous block element count",
581    )?;
582    let block_elements = checked_usize_from_u64(
583        block_elements_u64,
584        "classic slice contiguous block element count",
585    )?;
586    let block_bytes = checked_usize_from_u64(
587        checked_mul_u64(
588            block_elements_u64,
589            T::element_size() as u64,
590            "classic slice contiguous block size in bytes",
591        )?,
592        "classic slice contiguous block size in bytes",
593    )?;
594
595    Ok(ContiguousSelectionPlan {
596        dims: dims.to_vec(),
597        strides,
598        tail_start,
599        block_elements,
600        block_bytes,
601        elem_size: T::element_size() as u64,
602    })
603}
604
605fn read_contiguous_selection_values_with_plan<T: NcReadType>(
606    file_data: &[u8],
607    var_name: &str,
608    base_offset: usize,
609    plan: &ContiguousSelectionPlan,
610    result_elements: usize,
611) -> Result<Vec<T>> {
612    if result_elements == 0 {
613        return Ok(Vec::new());
614    }
615
616    let mut values = Vec::with_capacity(result_elements);
617    let context = BlockReadContext {
618        file_data,
619        var_name,
620        base_offset,
621        plan,
622    };
623    read_selected_blocks_recursive::<T>(0, 0, &context, &mut values)?;
624    Ok(values)
625}
626
627fn append_one_record_slice<T: NcReadType>(
628    context: &RecordSliceContext<'_>,
629    record: u64,
630    values: &mut Vec<T>,
631) -> Result<()> {
632    let record = checked_usize_from_u64(record, "classic record index")?;
633    let record_offset = context
634        .base_offset
635        .checked_add(record.checked_mul(context.record_stride).ok_or_else(|| {
636            Error::InvalidData("classic record byte offset exceeds platform usize".to_string())
637        })?)
638        .ok_or_else(|| {
639            Error::InvalidData("classic record byte offset exceeds platform usize".to_string())
640        })?;
641    let mut decoded = read_contiguous_selection_values_with_plan::<T>(
642        context.file_data,
643        context.var_name,
644        record_offset,
645        context.inner_plan,
646        context.inner_resolved.result_elements,
647    )?;
648    values.append(&mut decoded);
649    Ok(())
650}
651
652fn read_selected_blocks_recursive<T: NcReadType>(
653    level: usize,
654    current_offset: u64,
655    context: &BlockReadContext<'_>,
656    values: &mut Vec<T>,
657) -> Result<()> {
658    if level == context.plan.tail_start {
659        let byte_offset = checked_usize_from_u64(
660            checked_mul_u64(
661                current_offset,
662                context.plan.elem_size,
663                "classic slice element byte offset",
664            )?,
665            "classic slice element byte offset",
666        )?;
667        let start = context
668            .base_offset
669            .checked_add(byte_offset)
670            .ok_or_else(|| {
671                Error::InvalidData("classic slice byte offset exceeds platform usize".to_string())
672            })?;
673        let end = start.checked_add(context.plan.block_bytes).ok_or_else(|| {
674            Error::InvalidData("classic slice byte range exceeds platform usize".to_string())
675        })?;
676        if end > context.file_data.len() {
677            return Err(Error::InvalidData(format!(
678                "variable '{}' slice data extends beyond file",
679                context.var_name
680            )));
681        }
682
683        let mut decoded =
684            T::decode_bulk_be(&context.file_data[start..end], context.plan.block_elements)?;
685        values.append(&mut decoded);
686        return Ok(());
687    }
688
689    match &context.plan.dims[level] {
690        ResolvedClassicSelectionDim::Index(idx) => read_selected_blocks_recursive::<T>(
691            level + 1,
692            current_offset
693                .checked_add(checked_mul_u64(
694                    *idx,
695                    context.plan.strides[level],
696                    "classic slice logical element offset",
697                )?)
698                .ok_or_else(|| {
699                    Error::InvalidData(
700                        "classic slice logical element offset exceeds u64".to_string(),
701                    )
702                })?,
703            context,
704            values,
705        ),
706        ResolvedClassicSelectionDim::Slice {
707            start, step, count, ..
708        } => {
709            let start = *start;
710            let step = *step;
711            let count = *count;
712            for ordinal in 0..count {
713                let coord = start
714                    .checked_add(checked_mul_u64(
715                        ordinal as u64,
716                        step,
717                        "classic slice coordinate",
718                    )?)
719                    .ok_or_else(|| {
720                        Error::InvalidData("classic slice coordinate exceeds u64".to_string())
721                    })?;
722                read_selected_blocks_recursive::<T>(
723                    level + 1,
724                    current_offset
725                        .checked_add(checked_mul_u64(
726                            coord,
727                            context.plan.strides[level],
728                            "classic slice logical element offset",
729                        )?)
730                        .ok_or_else(|| {
731                            Error::InvalidData(
732                                "classic slice logical element offset exceeds u64".to_string(),
733                            )
734                        })?,
735                    context,
736                    values,
737                )?;
738            }
739            Ok(())
740        }
741    }
742}
743
744#[cfg(test)]
745mod tests {
746    use super::*;
747    use crate::types::NcDimension;
748
749    fn char_variable(shape: &[u64]) -> NcVariable {
750        NcVariable {
751            name: "chars".to_string(),
752            dimensions: shape
753                .iter()
754                .enumerate()
755                .map(|(i, &size)| NcDimension {
756                    name: format!("d{i}"),
757                    size,
758                    is_unlimited: false,
759                })
760                .collect(),
761            dtype: NcType::Char,
762            attributes: vec![],
763            data_offset: 0,
764            _data_size: 0,
765            is_record_var: false,
766            record_size: 0,
767        }
768    }
769
770    #[test]
771    fn test_decode_char_variable_strings_1d() {
772        let var = char_variable(&[5]);
773        let strings = decode_char_variable_strings(&var, b"alpha").unwrap();
774        assert_eq!(strings, vec!["alpha"]);
775    }
776
777    #[test]
778    fn test_decode_char_variable_strings_2d() {
779        let var = char_variable(&[2, 5]);
780        let strings = decode_char_variable_strings(&var, b"alphabeta\0").unwrap();
781        assert_eq!(strings, vec!["alpha", "beta"]);
782    }
783}