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#[cfg(feature = "rayon")]
8use rayon::prelude::*;
9
10use crate::error::{Error, Result};
11use crate::types::{
12    checked_mul_u64, checked_shape_elements, checked_usize_from_u64, NcType, NcVariable,
13};
14
15use super::data::{self, compute_record_stride, NcReadType};
16use super::storage::ClassicStorage;
17use super::ClassicFile;
18
19#[derive(Clone, Debug)]
20enum ResolvedClassicSelectionDim {
21    Index(u64),
22    Slice {
23        start: u64,
24        step: u64,
25        count: usize,
26        is_full_unit_stride: bool,
27    },
28}
29
30impl ResolvedClassicSelectionDim {
31    fn is_full_unit_stride(&self) -> bool {
32        matches!(
33            self,
34            Self::Slice {
35                is_full_unit_stride: true,
36                ..
37            }
38        )
39    }
40}
41
42#[derive(Clone, Debug)]
43struct ResolvedClassicSelection {
44    dims: Vec<ResolvedClassicSelectionDim>,
45    result_shape: Vec<usize>,
46    result_elements: usize,
47}
48
49struct BlockReadContext<'a> {
50    base_offset: u64,
51    plan: &'a ContiguousSelectionPlan,
52}
53
54struct ContiguousSelectionPlan {
55    dims: Vec<ResolvedClassicSelectionDim>,
56    strides: Vec<u64>,
57    tail_start: usize,
58    block_elements: usize,
59    block_bytes: usize,
60    elem_size: u64,
61}
62
63#[derive(Clone, Debug)]
64struct PlannedReadSpan {
65    offset: u64,
66    elements: usize,
67    bytes: usize,
68}
69
70#[cfg(feature = "rayon")]
71#[derive(Debug)]
72struct PlannedReadChunk {
73    spans: Vec<PlannedReadSpan>,
74    elements: usize,
75}
76
77struct RecordSliceContext<'a> {
78    storage: &'a ClassicStorage,
79    base_offset: u64,
80    record_stride: u64,
81    inner_resolved: &'a ResolvedClassicSelection,
82    inner_plan: &'a ContiguousSelectionPlan,
83}
84
85impl ClassicFile {
86    /// Read a variable's data as an ndarray of the specified type.
87    ///
88    /// The type parameter `T` must match the variable's NetCDF type. For example,
89    /// use `f32` for NC_FLOAT variables and `f64` for NC_DOUBLE variables.
90    pub fn read_variable<T: NcReadType>(&self, name: &str) -> Result<ArrayD<T>> {
91        let var = self.find_variable(name)?;
92
93        // Check type compatibility.
94        let expected = T::nc_type();
95        if var.dtype != expected {
96            return Err(Error::TypeMismatch {
97                expected: format!("{:?}", expected),
98                actual: format!("{:?}", var.dtype),
99            });
100        }
101
102        if var.is_record_var {
103            let record_stride = compute_record_stride(&self.root_group.variables)?;
104            data::read_record_variable_from_storage(&self.storage, var, self.numrecs, record_stride)
105        } else {
106            data::read_non_record_variable_from_storage(&self.storage, var)
107        }
108    }
109
110    /// Read a variable's data using Rayon for large classic byte ranges.
111    #[cfg(feature = "rayon")]
112    pub fn read_variable_parallel<T: NcReadType>(&self, name: &str) -> Result<ArrayD<T>> {
113        let var = self.find_variable(name)?;
114
115        let expected = T::nc_type();
116        if var.dtype != expected {
117            return Err(Error::TypeMismatch {
118                expected: format!("{:?}", expected),
119                actual: format!("{:?}", var.dtype),
120            });
121        }
122
123        if var.is_record_var {
124            let record_stride = compute_record_stride(&self.root_group.variables)?;
125            data::read_record_variable_parallel_from_storage(
126                &self.storage,
127                var,
128                self.numrecs,
129                record_stride,
130            )
131        } else {
132            data::read_non_record_variable_parallel_from_storage(&self.storage, var)
133        }
134    }
135
136    /// Read a variable's data into a caller-provided typed buffer.
137    pub fn read_variable_into<T: NcReadType>(&self, name: &str, dst: &mut [T]) -> Result<()> {
138        let var = self.find_variable(name)?;
139
140        let expected = T::nc_type();
141        if var.dtype != expected {
142            return Err(Error::TypeMismatch {
143                expected: format!("{:?}", expected),
144                actual: format!("{:?}", var.dtype),
145            });
146        }
147
148        if var.is_record_var {
149            let record_stride = compute_record_stride(&self.root_group.variables)?;
150            data::read_record_variable_into_from_storage(
151                &self.storage,
152                var,
153                self.numrecs,
154                record_stride,
155                dst,
156            )
157        } else {
158            data::read_non_record_variable_into_from_storage(&self.storage, var, dst)
159        }
160    }
161
162    /// Read a variable's data with automatic type promotion to f64.
163    ///
164    /// This reads any numeric variable and converts all values to f64,
165    /// which is convenient for analysis but may lose precision for i64/u64.
166    pub fn read_variable_as_f64(&self, name: &str) -> Result<ArrayD<f64>> {
167        let var = self.find_variable(name)?;
168
169        match var.dtype {
170            NcType::Byte => {
171                let arr = self.read_typed_variable::<i8>(var)?;
172                Ok(arr.mapv(|v| v as f64))
173            }
174            NcType::Short => {
175                let arr = self.read_typed_variable::<i16>(var)?;
176                Ok(arr.mapv(|v| v as f64))
177            }
178            NcType::Int => {
179                let arr = self.read_typed_variable::<i32>(var)?;
180                Ok(arr.mapv(|v| v as f64))
181            }
182            NcType::Float => {
183                let arr = self.read_typed_variable::<f32>(var)?;
184                Ok(arr.mapv(|v| v as f64))
185            }
186            NcType::Double => self.read_typed_variable::<f64>(var),
187            NcType::UByte => {
188                let arr = self.read_typed_variable::<u8>(var)?;
189                Ok(arr.mapv(|v| v as f64))
190            }
191            NcType::UShort => {
192                let arr = self.read_typed_variable::<u16>(var)?;
193                Ok(arr.mapv(|v| v as f64))
194            }
195            NcType::UInt => {
196                let arr = self.read_typed_variable::<u32>(var)?;
197                Ok(arr.mapv(|v| v as f64))
198            }
199            NcType::Int64 => {
200                let arr = self.read_typed_variable::<i64>(var)?;
201                Ok(arr.mapv(|v| v as f64))
202            }
203            NcType::UInt64 => {
204                let arr = self.read_typed_variable::<u64>(var)?;
205                Ok(arr.mapv(|v| v as f64))
206            }
207            NcType::Char => Err(Error::TypeMismatch {
208                expected: "numeric type".to_string(),
209                actual: "Char".to_string(),
210            }),
211            NcType::String => Err(Error::TypeMismatch {
212                expected: "numeric type".to_string(),
213                actual: "String".to_string(),
214            }),
215            _ => Err(Error::TypeMismatch {
216                expected: "numeric type".to_string(),
217                actual: format!("{:?}", var.dtype),
218            }),
219        }
220    }
221
222    /// Read a char variable as a String (or Vec<String> for multi-dimensional).
223    pub fn read_variable_as_string(&self, name: &str) -> Result<String> {
224        let mut strings = self.read_variable_as_strings(name)?;
225        match strings.len() {
226            1 => Ok(strings.swap_remove(0)),
227            0 => Err(Error::InvalidData(format!(
228                "variable '{}' contains no string elements",
229                name
230            ))),
231            count => Err(Error::InvalidData(format!(
232                "variable '{}' contains {count} strings; use read_variable_as_strings()",
233                name
234            ))),
235        }
236    }
237
238    /// Read a char variable as a flat vector of strings.
239    ///
240    /// For 2-D and higher char arrays, the last dimension is interpreted as the
241    /// string length and the leading dimensions are flattened.
242    pub fn read_variable_as_strings(&self, name: &str) -> Result<Vec<String>> {
243        let var = self.find_variable(name)?;
244        if var.dtype != NcType::Char {
245            return Err(Error::TypeMismatch {
246                expected: "Char".to_string(),
247                actual: format!("{:?}", var.dtype),
248            });
249        }
250
251        let arr = self.read_typed_variable::<u8>(var)?;
252        let bytes: Vec<u8> = arr.iter().copied().collect();
253        decode_char_variable_strings(var, &bytes)
254    }
255
256    /// Read a slice (hyperslab) of a variable.
257    ///
258    /// Classic variables are read directly from the on-disk byte ranges for
259    /// arbitrary selections.
260    pub fn read_variable_slice<T: NcReadType>(
261        &self,
262        name: &str,
263        selection: &crate::types::NcSliceInfo,
264    ) -> Result<ArrayD<T>> {
265        let var = self.find_variable(name)?;
266        let expected = T::nc_type();
267        if var.dtype != expected {
268            return Err(Error::TypeMismatch {
269                expected: format!("{:?}", expected),
270                actual: format!("{:?}", var.dtype),
271            });
272        }
273        let resolved = resolve_classic_selection(
274            var,
275            selection,
276            if var.is_record_var { self.numrecs } else { 0 },
277        )?;
278
279        if !var.is_record_var {
280            return read_non_record_variable_slice_direct(&self.storage, var, &resolved);
281        }
282
283        let record_stride = compute_record_stride(&self.root_group.variables)?;
284        read_record_variable_slice_direct(
285            &self.storage,
286            var,
287            self.numrecs,
288            record_stride,
289            &resolved,
290        )
291    }
292
293    /// Read a variable slice using Rayon for large planned byte ranges.
294    #[cfg(feature = "rayon")]
295    pub fn read_variable_slice_parallel<T: NcReadType>(
296        &self,
297        name: &str,
298        selection: &crate::types::NcSliceInfo,
299    ) -> Result<ArrayD<T>> {
300        let var = self.find_variable(name)?;
301        let expected = T::nc_type();
302        if var.dtype != expected {
303            return Err(Error::TypeMismatch {
304                expected: format!("{:?}", expected),
305                actual: format!("{:?}", var.dtype),
306            });
307        }
308        let resolved = resolve_classic_selection(
309            var,
310            selection,
311            if var.is_record_var { self.numrecs } else { 0 },
312        )?;
313
314        if !var.is_record_var {
315            return read_non_record_variable_slice_parallel(&self.storage, var, &resolved);
316        }
317
318        let record_stride = compute_record_stride(&self.root_group.variables)?;
319        read_record_variable_slice_parallel(
320            &self.storage,
321            var,
322            self.numrecs,
323            record_stride,
324            &resolved,
325        )
326    }
327
328    /// Read a slice with automatic type promotion to f64.
329    pub fn read_variable_slice_as_f64(
330        &self,
331        name: &str,
332        selection: &crate::types::NcSliceInfo,
333    ) -> Result<ArrayD<f64>> {
334        let var = self.find_variable(name)?;
335
336        macro_rules! slice_promoted {
337            ($ty:ty) => {{
338                let sliced = self.read_variable_slice::<$ty>(name, selection)?;
339                Ok(sliced.mapv(|v| v as f64))
340            }};
341        }
342
343        match var.dtype {
344            NcType::Byte => slice_promoted!(i8),
345            NcType::Short => slice_promoted!(i16),
346            NcType::Int => slice_promoted!(i32),
347            NcType::Float => slice_promoted!(f32),
348            NcType::Double => slice_promoted!(f64),
349            NcType::UByte => slice_promoted!(u8),
350            NcType::UShort => slice_promoted!(u16),
351            NcType::UInt => slice_promoted!(u32),
352            NcType::Int64 => slice_promoted!(i64),
353            NcType::UInt64 => slice_promoted!(u64),
354            NcType::Char => Err(Error::TypeMismatch {
355                expected: "numeric type".to_string(),
356                actual: "Char".to_string(),
357            }),
358            _ => Err(Error::TypeMismatch {
359                expected: "numeric type".to_string(),
360                actual: format!("{:?}", var.dtype),
361            }),
362        }
363    }
364
365    /// Internal: find a variable by name.
366    fn find_variable(&self, name: &str) -> Result<&NcVariable> {
367        self.root_group
368            .variables
369            .iter()
370            .find(|v| v.name == name)
371            .ok_or_else(|| Error::VariableNotFound(name.to_string()))
372    }
373
374    /// Internal: read a variable with the correct record handling.
375    fn read_typed_variable<T: NcReadType>(&self, var: &NcVariable) -> Result<ArrayD<T>> {
376        if var.is_record_var {
377            let record_stride = compute_record_stride(&self.root_group.variables)?;
378            data::read_record_variable_from_storage(&self.storage, var, self.numrecs, record_stride)
379        } else {
380            data::read_non_record_variable_from_storage(&self.storage, var)
381        }
382    }
383}
384
385fn decode_char_variable_strings(var: &NcVariable, bytes: &[u8]) -> Result<Vec<String>> {
386    let shape = var.shape();
387    if shape.len() <= 1 {
388        return Ok(vec![decode_char_string(bytes)]);
389    }
390
391    let string_len = checked_usize_from_u64(
392        *shape
393            .last()
394            .ok_or_else(|| Error::InvalidData("char variable missing string axis".into()))?,
395        "char string length",
396    )?;
397    let string_count_u64 = checked_shape_elements(&shape[..shape.len() - 1], "char string count")?;
398    let string_count = checked_usize_from_u64(string_count_u64, "char string count")?;
399    let expected_bytes = string_count.checked_mul(string_len).ok_or_else(|| {
400        Error::InvalidData("char string byte count exceeds platform usize".to_string())
401    })?;
402
403    if bytes.len() < expected_bytes {
404        return Err(Error::InvalidData(format!(
405            "char variable '{}' data too short: need {} bytes, have {}",
406            var.name,
407            expected_bytes,
408            bytes.len()
409        )));
410    }
411
412    if string_len == 0 {
413        return Ok(vec![String::new(); string_count]);
414    }
415
416    Ok(bytes[..expected_bytes]
417        .chunks_exact(string_len)
418        .map(decode_char_string)
419        .collect())
420}
421
422fn decode_char_string(bytes: &[u8]) -> String {
423    String::from_utf8_lossy(bytes)
424        .trim_end_matches('\0')
425        .to_string()
426}
427
428fn variable_shape_for_selection(var: &NcVariable, numrecs: u64) -> Vec<u64> {
429    let mut shape = var.shape();
430    if var.is_record_var && !shape.is_empty() {
431        shape[0] = numrecs;
432    }
433    shape
434}
435
436fn row_major_strides(shape: &[u64], context: &str) -> Result<Vec<u64>> {
437    let ndim = shape.len();
438    if ndim == 0 {
439        return Ok(Vec::new());
440    }
441
442    let mut strides = vec![1u64; ndim];
443    for i in (0..ndim - 1).rev() {
444        strides[i] = checked_mul_u64(strides[i + 1], shape[i + 1], context)?;
445    }
446    Ok(strides)
447}
448
449fn resolve_classic_selection(
450    var: &NcVariable,
451    selection: &crate::types::NcSliceInfo,
452    numrecs: u64,
453) -> Result<ResolvedClassicSelection> {
454    use crate::types::NcSliceInfoElem;
455
456    let shape = variable_shape_for_selection(var, numrecs);
457    if selection.selections.len() != shape.len() {
458        return Err(Error::InvalidData(format!(
459            "selection has {} dimensions but variable '{}' has {}",
460            selection.selections.len(),
461            var.name,
462            shape.len()
463        )));
464    }
465
466    let mut dims = Vec::with_capacity(shape.len());
467    let mut result_shape = Vec::new();
468    let mut result_elements = 1usize;
469
470    for (dim, (sel, &dim_size)) in selection.selections.iter().zip(shape.iter()).enumerate() {
471        match sel {
472            NcSliceInfoElem::Index(idx) => {
473                if *idx >= dim_size {
474                    return Err(Error::InvalidData(format!(
475                        "index {} out of bounds for dimension {} (size {})",
476                        idx, dim, dim_size
477                    )));
478                }
479                dims.push(ResolvedClassicSelectionDim::Index(*idx));
480            }
481            NcSliceInfoElem::Slice { start, end, step } => {
482                if *step == 0 {
483                    return Err(Error::InvalidData("slice step cannot be 0".to_string()));
484                }
485                if *start > dim_size {
486                    return Err(Error::InvalidData(format!(
487                        "slice start {} out of bounds for dimension {} (size {})",
488                        start, dim, dim_size
489                    )));
490                }
491
492                let actual_end = if *end == u64::MAX {
493                    dim_size
494                } else {
495                    (*end).min(dim_size)
496                };
497                let count_u64 = if *start >= actual_end {
498                    0
499                } else {
500                    (actual_end - *start).div_ceil(*step)
501                };
502                let count = checked_usize_from_u64(count_u64, "classic slice result dimension")?;
503
504                result_shape.push(count);
505                result_elements = result_elements.checked_mul(count).ok_or_else(|| {
506                    Error::InvalidData(
507                        "classic slice result element count exceeds platform usize".to_string(),
508                    )
509                })?;
510                dims.push(ResolvedClassicSelectionDim::Slice {
511                    start: *start,
512                    step: *step,
513                    count,
514                    is_full_unit_stride: *start == 0 && actual_end == dim_size && *step == 1,
515                });
516            }
517        }
518    }
519
520    Ok(ResolvedClassicSelection {
521        dims,
522        result_shape,
523        result_elements,
524    })
525}
526
527fn read_non_record_variable_slice_direct<T: NcReadType>(
528    storage: &ClassicStorage,
529    var: &NcVariable,
530    resolved: &ResolvedClassicSelection,
531) -> Result<ArrayD<T>> {
532    let shape = variable_shape_for_selection(var, 0);
533    build_array_from_contiguous_selection::<T>(storage, var.data_offset, &shape, resolved)
534}
535
536#[cfg(feature = "rayon")]
537fn read_non_record_variable_slice_parallel<T: NcReadType>(
538    storage: &ClassicStorage,
539    var: &NcVariable,
540    resolved: &ResolvedClassicSelection,
541) -> Result<ArrayD<T>> {
542    use ndarray::IxDyn;
543
544    let shape = variable_shape_for_selection(var, 0);
545    let plan = build_contiguous_selection_plan::<T>(&shape, &resolved.dims)?;
546    let spans = plan_contiguous_selection_spans(var.data_offset, &plan, resolved.result_elements)?;
547    let values = read_planned_spans_maybe_parallel::<T>(storage, &spans, resolved.result_elements)?;
548
549    ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
550        .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
551}
552
553fn read_record_variable_slice_direct<T: NcReadType>(
554    storage: &ClassicStorage,
555    var: &NcVariable,
556    numrecs: u64,
557    record_stride: u64,
558    resolved: &ResolvedClassicSelection,
559) -> Result<ArrayD<T>> {
560    use ndarray::IxDyn;
561
562    if resolved.result_elements == 0 {
563        return ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), Vec::new())
564            .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")));
565    }
566
567    let shape = variable_shape_for_selection(var, numrecs);
568    let inner_shape = &shape[1..];
569    let inner_dims = resolved.dims[1..].to_vec();
570    let inner_resolved = ResolvedClassicSelection {
571        result_shape: selection_result_shape(&inner_dims),
572        result_elements: selection_result_elements(&inner_dims)?,
573        dims: inner_dims,
574    };
575    let inner_plan = build_contiguous_selection_plan::<T>(inner_shape, &inner_resolved.dims)?;
576    let mut values = Vec::with_capacity(resolved.result_elements);
577    let context = RecordSliceContext {
578        storage,
579        base_offset: var.data_offset,
580        record_stride,
581        inner_resolved: &inner_resolved,
582        inner_plan: &inner_plan,
583    };
584
585    match &resolved.dims[0] {
586        ResolvedClassicSelectionDim::Index(record) => {
587            append_one_record_slice::<T>(&context, *record, &mut values)?
588        }
589        ResolvedClassicSelectionDim::Slice {
590            start, step, count, ..
591        } => {
592            for ordinal in 0..*count {
593                let record = start
594                    .checked_add(checked_mul_u64(
595                        ordinal as u64,
596                        *step,
597                        "classic record slice coordinate",
598                    )?)
599                    .ok_or_else(|| {
600                        Error::InvalidData(
601                            "classic record slice coordinate exceeds u64".to_string(),
602                        )
603                    })?;
604                append_one_record_slice::<T>(&context, record, &mut values)?;
605            }
606        }
607    }
608
609    debug_assert_eq!(values.len(), resolved.result_elements);
610    ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
611        .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
612}
613
614#[cfg(feature = "rayon")]
615fn read_record_variable_slice_parallel<T: NcReadType>(
616    storage: &ClassicStorage,
617    var: &NcVariable,
618    numrecs: u64,
619    record_stride: u64,
620    resolved: &ResolvedClassicSelection,
621) -> Result<ArrayD<T>> {
622    use ndarray::IxDyn;
623
624    if resolved.result_elements == 0 {
625        return ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), Vec::new())
626            .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")));
627    }
628
629    let shape = variable_shape_for_selection(var, numrecs);
630    let inner_shape = &shape[1..];
631    let inner_dims = resolved.dims[1..].to_vec();
632    let inner_resolved = ResolvedClassicSelection {
633        result_shape: selection_result_shape(&inner_dims),
634        result_elements: selection_result_elements(&inner_dims)?,
635        dims: inner_dims,
636    };
637    let inner_plan = build_contiguous_selection_plan::<T>(inner_shape, &inner_resolved.dims)?;
638    let mut spans = Vec::new();
639
640    match &resolved.dims[0] {
641        ResolvedClassicSelectionDim::Index(record) => append_one_record_slice_spans(
642            var.data_offset,
643            record_stride,
644            &inner_plan,
645            *record,
646            &mut spans,
647        )?,
648        ResolvedClassicSelectionDim::Slice {
649            start, step, count, ..
650        } => {
651            for ordinal in 0..*count {
652                let record = start
653                    .checked_add(checked_mul_u64(
654                        ordinal as u64,
655                        *step,
656                        "classic record slice coordinate",
657                    )?)
658                    .ok_or_else(|| {
659                        Error::InvalidData(
660                            "classic record slice coordinate exceeds u64".to_string(),
661                        )
662                    })?;
663                append_one_record_slice_spans(
664                    var.data_offset,
665                    record_stride,
666                    &inner_plan,
667                    record,
668                    &mut spans,
669                )?;
670            }
671        }
672    }
673
674    let values = read_planned_spans_maybe_parallel::<T>(storage, &spans, resolved.result_elements)?;
675    ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
676        .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
677}
678
679fn selection_result_shape(dims: &[ResolvedClassicSelectionDim]) -> Vec<usize> {
680    dims.iter()
681        .filter_map(|dim| match dim {
682            ResolvedClassicSelectionDim::Index(_) => None,
683            ResolvedClassicSelectionDim::Slice { count, .. } => Some(*count),
684        })
685        .collect()
686}
687
688fn selection_result_elements(dims: &[ResolvedClassicSelectionDim]) -> Result<usize> {
689    let mut elements = 1usize;
690    for dim in dims {
691        if let ResolvedClassicSelectionDim::Slice { count, .. } = dim {
692            elements = elements.checked_mul(*count).ok_or_else(|| {
693                Error::InvalidData(
694                    "classic slice result element count exceeds platform usize".to_string(),
695                )
696            })?;
697        }
698    }
699    Ok(elements)
700}
701
702fn build_array_from_contiguous_selection<T: NcReadType>(
703    storage: &ClassicStorage,
704    base_offset: u64,
705    shape: &[u64],
706    resolved: &ResolvedClassicSelection,
707) -> Result<ArrayD<T>> {
708    use ndarray::IxDyn;
709
710    let plan = build_contiguous_selection_plan::<T>(shape, &resolved.dims)?;
711    let values = read_contiguous_selection_values_with_plan::<T>(
712        storage,
713        base_offset,
714        &plan,
715        resolved.result_elements,
716    )?;
717    ArrayD::from_shape_vec(IxDyn(&resolved.result_shape), values)
718        .map_err(|e| Error::InvalidData(format!("failed to create array: {e}")))
719}
720
721fn build_contiguous_selection_plan<T: NcReadType>(
722    shape: &[u64],
723    dims: &[ResolvedClassicSelectionDim],
724) -> Result<ContiguousSelectionPlan> {
725    let strides = row_major_strides(shape, "classic slice stride")?;
726    let tail_start = dims
727        .iter()
728        .rposition(|dim| !dim.is_full_unit_stride())
729        .map_or(0, |idx| idx + 1);
730    let block_elements_u64 = checked_shape_elements(
731        &shape[tail_start..],
732        "classic slice contiguous block element count",
733    )?;
734    let block_elements = checked_usize_from_u64(
735        block_elements_u64,
736        "classic slice contiguous block element count",
737    )?;
738    let block_bytes = checked_usize_from_u64(
739        checked_mul_u64(
740            block_elements_u64,
741            T::element_size() as u64,
742            "classic slice contiguous block size in bytes",
743        )?,
744        "classic slice contiguous block size in bytes",
745    )?;
746
747    Ok(ContiguousSelectionPlan {
748        dims: dims.to_vec(),
749        strides,
750        tail_start,
751        block_elements,
752        block_bytes,
753        elem_size: T::element_size() as u64,
754    })
755}
756
757fn read_contiguous_selection_values_with_plan<T: NcReadType>(
758    storage: &ClassicStorage,
759    base_offset: u64,
760    plan: &ContiguousSelectionPlan,
761    result_elements: usize,
762) -> Result<Vec<T>> {
763    if result_elements == 0 {
764        return Ok(Vec::new());
765    }
766
767    let spans = plan_contiguous_selection_spans(base_offset, plan, result_elements)?;
768    read_planned_spans_serial::<T>(storage, &spans, result_elements)
769}
770
771fn plan_contiguous_selection_spans(
772    base_offset: u64,
773    plan: &ContiguousSelectionPlan,
774    result_elements: usize,
775) -> Result<Vec<PlannedReadSpan>> {
776    if result_elements == 0 {
777        return Ok(Vec::new());
778    }
779
780    let mut spans = Vec::new();
781    let context = BlockReadContext { base_offset, plan };
782    plan_selected_blocks_recursive(0, 0, &context, &mut spans)?;
783    Ok(spans)
784}
785
786fn read_planned_spans_serial<T: NcReadType>(
787    storage: &ClassicStorage,
788    spans: &[PlannedReadSpan],
789    result_elements: usize,
790) -> Result<Vec<T>> {
791    let total_elements = planned_spans_total_elements(spans)?;
792    if total_elements != result_elements {
793        return Err(Error::InvalidData(format!(
794            "classic planned span element count {total_elements} does not match result size {result_elements}"
795        )));
796    }
797
798    let mut values = vec![T::default(); result_elements];
799    let mut dst_start = 0usize;
800    for span in spans {
801        let data = storage.read_range(span.offset, span.bytes)?;
802        let dst_end = dst_start.checked_add(span.elements).ok_or_else(|| {
803            Error::InvalidData(
804                "classic planned destination range exceeds platform usize".to_string(),
805            )
806        })?;
807        T::decode_bulk_be_into(data.as_ref(), &mut values[dst_start..dst_end])?;
808        dst_start = dst_end;
809    }
810    debug_assert_eq!(values.len(), result_elements);
811    Ok(values)
812}
813
814fn planned_spans_total_elements(spans: &[PlannedReadSpan]) -> Result<usize> {
815    let mut total = 0usize;
816    for span in spans {
817        total = total.checked_add(span.elements).ok_or_else(|| {
818            Error::InvalidData("classic planned element count exceeds platform usize".to_string())
819        })?;
820    }
821    Ok(total)
822}
823
824fn append_one_record_slice<T: NcReadType>(
825    context: &RecordSliceContext<'_>,
826    record: u64,
827    values: &mut Vec<T>,
828) -> Result<()> {
829    let record_offset = context
830        .base_offset
831        .checked_add(record.checked_mul(context.record_stride).ok_or_else(|| {
832            Error::InvalidData("classic record byte offset exceeds u64".to_string())
833        })?)
834        .ok_or_else(|| Error::InvalidData("classic record byte offset exceeds u64".to_string()))?;
835    let mut decoded = read_contiguous_selection_values_with_plan::<T>(
836        context.storage,
837        record_offset,
838        context.inner_plan,
839        context.inner_resolved.result_elements,
840    )?;
841    values.append(&mut decoded);
842    Ok(())
843}
844
845#[cfg(feature = "rayon")]
846fn append_one_record_slice_spans(
847    base_offset: u64,
848    record_stride: u64,
849    inner_plan: &ContiguousSelectionPlan,
850    record: u64,
851    spans: &mut Vec<PlannedReadSpan>,
852) -> Result<()> {
853    let record_offset = base_offset
854        .checked_add(record.checked_mul(record_stride).ok_or_else(|| {
855            Error::InvalidData("classic record byte offset exceeds u64".to_string())
856        })?)
857        .ok_or_else(|| Error::InvalidData("classic record byte offset exceeds u64".to_string()))?;
858    let record_spans = plan_contiguous_selection_spans(record_offset, inner_plan, 1)?;
859    for span in record_spans {
860        push_planned_span(spans, span)?;
861    }
862    Ok(())
863}
864
865fn plan_selected_blocks_recursive(
866    level: usize,
867    current_offset: u64,
868    context: &BlockReadContext<'_>,
869    spans: &mut Vec<PlannedReadSpan>,
870) -> Result<()> {
871    if level == context.plan.tail_start {
872        let byte_offset = checked_mul_u64(
873            current_offset,
874            context.plan.elem_size,
875            "classic slice element byte offset",
876        )?;
877        let start = context
878            .base_offset
879            .checked_add(byte_offset)
880            .ok_or_else(|| {
881                Error::InvalidData("classic slice byte offset exceeds u64".to_string())
882            })?;
883
884        push_planned_span(
885            spans,
886            PlannedReadSpan {
887                offset: start,
888                elements: context.plan.block_elements,
889                bytes: context.plan.block_bytes,
890            },
891        )?;
892        return Ok(());
893    }
894
895    match &context.plan.dims[level] {
896        ResolvedClassicSelectionDim::Index(idx) => plan_selected_blocks_recursive(
897            level + 1,
898            current_offset
899                .checked_add(checked_mul_u64(
900                    *idx,
901                    context.plan.strides[level],
902                    "classic slice logical element offset",
903                )?)
904                .ok_or_else(|| {
905                    Error::InvalidData(
906                        "classic slice logical element offset exceeds u64".to_string(),
907                    )
908                })?,
909            context,
910            spans,
911        ),
912        ResolvedClassicSelectionDim::Slice {
913            start, step, count, ..
914        } => {
915            let start = *start;
916            let step = *step;
917            let count = *count;
918            for ordinal in 0..count {
919                let coord = start
920                    .checked_add(checked_mul_u64(
921                        ordinal as u64,
922                        step,
923                        "classic slice coordinate",
924                    )?)
925                    .ok_or_else(|| {
926                        Error::InvalidData("classic slice coordinate exceeds u64".to_string())
927                    })?;
928                plan_selected_blocks_recursive(
929                    level + 1,
930                    current_offset
931                        .checked_add(checked_mul_u64(
932                            coord,
933                            context.plan.strides[level],
934                            "classic slice logical element offset",
935                        )?)
936                        .ok_or_else(|| {
937                            Error::InvalidData(
938                                "classic slice logical element offset exceeds u64".to_string(),
939                            )
940                        })?,
941                    context,
942                    spans,
943                )?;
944            }
945            Ok(())
946        }
947    }
948}
949
950fn push_planned_span(spans: &mut Vec<PlannedReadSpan>, span: PlannedReadSpan) -> Result<()> {
951    if span.bytes == 0 {
952        return Ok(());
953    }
954
955    if let Some(last) = spans.last_mut() {
956        let last_end = last.offset.checked_add(last.bytes as u64).ok_or_else(|| {
957            Error::InvalidData("classic planned byte span exceeds u64".to_string())
958        })?;
959        if last_end == span.offset {
960            last.bytes = last.bytes.checked_add(span.bytes).ok_or_else(|| {
961                Error::InvalidData("classic planned byte span exceeds platform usize".to_string())
962            })?;
963            last.elements = last.elements.checked_add(span.elements).ok_or_else(|| {
964                Error::InvalidData(
965                    "classic planned element span exceeds platform usize".to_string(),
966                )
967            })?;
968            return Ok(());
969        }
970    }
971
972    spans.push(span);
973    Ok(())
974}
975
976#[cfg(feature = "rayon")]
977fn read_planned_spans_maybe_parallel<T: NcReadType>(
978    storage: &ClassicStorage,
979    spans: &[PlannedReadSpan],
980    result_elements: usize,
981) -> Result<Vec<T>> {
982    let total_bytes = planned_spans_total_bytes(spans)?;
983    let policy = storage.parallel_read_policy();
984    if total_bytes < policy.min_bytes || spans.is_empty() {
985        return read_planned_spans_serial::<T>(storage, spans, result_elements);
986    }
987
988    let total_elements = planned_spans_total_elements(spans)?;
989    if total_elements != result_elements {
990        return Err(Error::InvalidData(format!(
991            "classic planned span element count {total_elements} does not match result size {result_elements}"
992        )));
993    }
994
995    let (chunks, elements_per_chunk) =
996        split_planned_spans_for_parallel::<T>(spans, policy.target_chunk_bytes)?;
997    let mut values = vec![T::default(); result_elements];
998    debug_assert_eq!(values.len().div_ceil(elements_per_chunk), chunks.len());
999    values
1000        .par_chunks_mut(elements_per_chunk)
1001        .zip(chunks.par_iter())
1002        .try_for_each(|(dst, chunk)| read_planned_chunk_into::<T>(storage, chunk, dst))?;
1003    Ok(values)
1004}
1005
1006#[cfg(feature = "rayon")]
1007fn planned_spans_total_bytes(spans: &[PlannedReadSpan]) -> Result<usize> {
1008    let mut total = 0usize;
1009    for span in spans {
1010        total = total.checked_add(span.bytes).ok_or_else(|| {
1011            Error::InvalidData("classic planned byte count exceeds platform usize".to_string())
1012        })?;
1013    }
1014    Ok(total)
1015}
1016
1017#[cfg(feature = "rayon")]
1018fn split_planned_spans_for_parallel<T: NcReadType>(
1019    spans: &[PlannedReadSpan],
1020    target_chunk_bytes: usize,
1021) -> Result<(Vec<PlannedReadChunk>, usize)> {
1022    let elem_size = T::element_size();
1023    let elements_per_chunk = (target_chunk_bytes / elem_size.max(1)).max(1);
1024    let mut chunks = Vec::new();
1025    let mut current = PlannedReadChunk {
1026        spans: Vec::new(),
1027        elements: 0,
1028    };
1029
1030    for span in spans {
1031        let mut remaining = span.elements;
1032        let mut offset = span.offset;
1033        while remaining > 0 {
1034            if current.elements == elements_per_chunk {
1035                chunks.push(current);
1036                current = PlannedReadChunk {
1037                    spans: Vec::new(),
1038                    elements: 0,
1039                };
1040            }
1041
1042            let available = elements_per_chunk - current.elements;
1043            let elements = remaining.min(available);
1044            let bytes = elements.checked_mul(elem_size).ok_or_else(|| {
1045                Error::InvalidData(
1046                    "classic planned chunk byte count exceeds platform usize".to_string(),
1047                )
1048            })?;
1049
1050            current.spans.push(PlannedReadSpan {
1051                offset,
1052                elements,
1053                bytes,
1054            });
1055            current.elements = current.elements.checked_add(elements).ok_or_else(|| {
1056                Error::InvalidData(
1057                    "classic planned chunk element count exceeds platform usize".to_string(),
1058                )
1059            })?;
1060            remaining -= elements;
1061            offset = offset.checked_add(bytes as u64).ok_or_else(|| {
1062                Error::InvalidData("classic planned chunk byte offset exceeds u64".to_string())
1063            })?;
1064        }
1065    }
1066
1067    if current.elements > 0 {
1068        chunks.push(current);
1069    }
1070
1071    Ok((chunks, elements_per_chunk))
1072}
1073
1074#[cfg(feature = "rayon")]
1075fn read_planned_chunk_into<T: NcReadType>(
1076    storage: &ClassicStorage,
1077    chunk: &PlannedReadChunk,
1078    dst: &mut [T],
1079) -> Result<()> {
1080    if dst.len() != chunk.elements {
1081        return Err(Error::InvalidData(format!(
1082            "classic planned chunk destination has {} elements, expected {}",
1083            dst.len(),
1084            chunk.elements
1085        )));
1086    }
1087
1088    let mut dst_start = 0usize;
1089    for span in &chunk.spans {
1090        let data = storage.read_range(span.offset, span.bytes)?;
1091        let dst_end = dst_start.checked_add(span.elements).ok_or_else(|| {
1092            Error::InvalidData(
1093                "classic planned chunk destination range exceeds platform usize".to_string(),
1094            )
1095        })?;
1096        T::decode_bulk_be_into(data.as_ref(), &mut dst[dst_start..dst_end])?;
1097        dst_start = dst_end;
1098    }
1099    Ok(())
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use super::*;
1105    use crate::types::NcDimension;
1106
1107    fn char_variable(shape: &[u64]) -> NcVariable {
1108        NcVariable {
1109            name: "chars".to_string(),
1110            dimensions: shape
1111                .iter()
1112                .enumerate()
1113                .map(|(i, &size)| NcDimension {
1114                    name: format!("d{i}"),
1115                    size,
1116                    is_unlimited: false,
1117                })
1118                .collect(),
1119            dtype: NcType::Char,
1120            attributes: vec![],
1121            data_offset: 0,
1122            _data_size: 0,
1123            is_record_var: false,
1124            record_size: 0,
1125        }
1126    }
1127
1128    #[test]
1129    fn decode_char_variable_strings_1d() {
1130        let var = char_variable(&[5]);
1131        let strings = decode_char_variable_strings(&var, b"alpha").unwrap();
1132        assert_eq!(strings, vec!["alpha"]);
1133    }
1134
1135    #[test]
1136    fn decode_char_variable_strings_2d() {
1137        let var = char_variable(&[2, 5]);
1138        let strings = decode_char_variable_strings(&var, b"alphabeta\0").unwrap();
1139        assert_eq!(strings, vec!["alpha", "beta"]);
1140    }
1141}