Skip to main content

scirs2_io/zarr/
array.rs

1//! ZarrArray: the main interface for reading and writing chunked Zarr arrays.
2
3use std::collections::HashMap;
4
5use crate::error::{IoError, Result};
6
7use super::codecs::{BytesCodec, CodecPipeline, Endian, ZstdCodec};
8use super::metadata::{ArrayMetadataV2, ArrayMetadataV3, CodecMetadata, CompressorV2};
9use super::store::{chunk_key_v2, chunk_key_v3, DirectoryStore};
10use super::{DataType, ZarrVersion};
11
12/// A chunked N-dimensional array stored in a Zarr directory store.
13#[derive(Debug)]
14pub struct ZarrArray {
15    store: DirectoryStore,
16    /// Path prefix within the store (e.g. `"myarray"`).
17    path: String,
18    /// Zarr version.
19    version: ZarrVersion,
20    /// Array shape.
21    shape: Vec<u64>,
22    /// Chunk shape.
23    chunks: Vec<u64>,
24    /// Scalar data type.
25    dtype: DataType,
26    /// Fill value as f64 (used when a chunk does not exist).
27    fill_value: f64,
28    /// Codec pipeline for encoding/decoding chunks.
29    pipeline: CodecPipeline,
30    /// Memory layout order (`"C"` or `"F"`).
31    order: String,
32    /// Dimension separator for chunk keys (v2).
33    dim_separator: String,
34    /// Chunk key separator for v3.
35    v3_separator: String,
36    /// Whether compression is enabled.
37    compressed: bool,
38}
39
40impl ZarrArray {
41    // ── Constructors ─────────────────────────────────────────────────────
42
43    /// Create a new Zarr array.
44    pub fn create(
45        store: DirectoryStore,
46        path: &str,
47        shape: Vec<u64>,
48        chunks: Vec<u64>,
49        dtype: DataType,
50        version: ZarrVersion,
51    ) -> Result<Self> {
52        Self::create_with_options(store, path, shape, chunks, dtype, version, 0.0, true)
53    }
54
55    /// Create a new Zarr array with explicit fill value and compression option.
56    pub fn create_with_options(
57        store: DirectoryStore,
58        path: &str,
59        shape: Vec<u64>,
60        chunks: Vec<u64>,
61        dtype: DataType,
62        version: ZarrVersion,
63        fill_value: f64,
64        compress: bool,
65    ) -> Result<Self> {
66        if shape.len() != chunks.len() {
67            return Err(IoError::FormatError(format!(
68                "Shape ndim ({}) != chunks ndim ({})",
69                shape.len(),
70                chunks.len()
71            )));
72        }
73
74        let pipeline = build_pipeline(dtype, compress);
75
76        let arr = Self {
77            store,
78            path: path.to_string(),
79            version,
80            shape: shape.clone(),
81            chunks: chunks.clone(),
82            dtype,
83            fill_value,
84            pipeline,
85            order: "C".to_string(),
86            dim_separator: ".".to_string(),
87            v3_separator: "/".to_string(),
88            compressed: compress,
89        };
90
91        // Write metadata
92        match version {
93            ZarrVersion::V2 => arr.write_v2_metadata()?,
94            ZarrVersion::V3 => arr.write_v3_metadata()?,
95        }
96
97        Ok(arr)
98    }
99
100    /// Open an existing Zarr array from a store.
101    pub fn open(store: DirectoryStore, path: &str) -> Result<Self> {
102        // Try v3 first (zarr.json)
103        let v3_key = if path.is_empty() {
104            "zarr.json".to_string()
105        } else {
106            format!("{path}/zarr.json")
107        };
108        if store.exists(&v3_key) {
109            let data = store.get(&v3_key)?;
110            let meta = ArrayMetadataV3::from_json(&data)?;
111            return Self::from_v3_metadata(store, path, &meta);
112        }
113
114        // Try v2 (.zarray)
115        let v2_key = if path.is_empty() {
116            ".zarray".to_string()
117        } else {
118            format!("{path}/.zarray")
119        };
120        if store.exists(&v2_key) {
121            let data = store.get(&v2_key)?;
122            let meta = ArrayMetadataV2::from_json(&data)?;
123            return Self::from_v2_metadata(store, path, &meta);
124        }
125
126        Err(IoError::FileNotFound(format!(
127            "No Zarr array metadata found at path '{path}'"
128        )))
129    }
130
131    fn from_v2_metadata(store: DirectoryStore, path: &str, meta: &ArrayMetadataV2) -> Result<Self> {
132        let dtype = meta.data_type()?;
133        let compressed = meta.compressor.is_some();
134        let pipeline = build_pipeline(dtype, compressed);
135        let fill_value = meta.fill_value.as_f64().unwrap_or(0.0);
136
137        Ok(Self {
138            store,
139            path: path.to_string(),
140            version: ZarrVersion::V2,
141            shape: meta.shape.clone(),
142            chunks: meta.chunks.clone(),
143            dtype,
144            fill_value,
145            pipeline,
146            order: meta.order.clone(),
147            dim_separator: meta.dimension_separator.clone(),
148            v3_separator: "/".to_string(),
149            compressed,
150        })
151    }
152
153    fn from_v3_metadata(store: DirectoryStore, path: &str, meta: &ArrayMetadataV3) -> Result<Self> {
154        let dtype = meta.data_type_parsed()?;
155        let chunk_shape = meta.chunk_shape()?.to_vec();
156        let compressed = meta
157            .codecs
158            .iter()
159            .any(|c| c.name == "zstd" || c.name == "gzip");
160        let pipeline = build_pipeline(dtype, compressed);
161        let fill_value = meta
162            .fill_value
163            .as_ref()
164            .and_then(|v| v.as_f64())
165            .unwrap_or(0.0);
166
167        let separator = meta
168            .chunk_key_encoding
169            .as_ref()
170            .and_then(|e| e.configuration.as_ref())
171            .map(|c| c.separator.clone())
172            .unwrap_or_else(|| "/".to_string());
173
174        Ok(Self {
175            store,
176            path: path.to_string(),
177            version: ZarrVersion::V3,
178            shape: meta.shape.clone(),
179            chunks: chunk_shape,
180            dtype,
181            fill_value,
182            pipeline,
183            order: "C".to_string(),
184            dim_separator: ".".to_string(),
185            v3_separator: separator,
186            compressed,
187        })
188    }
189
190    fn write_v2_metadata(&self) -> Result<()> {
191        let compressor = if self.compressed {
192            Some(CompressorV2 {
193                id: "zstd".to_string(),
194                level: Some(3),
195                extra: HashMap::new(),
196            })
197        } else {
198            None
199        };
200
201        let meta = ArrayMetadataV2::new(
202            self.shape.clone(),
203            self.chunks.clone(),
204            self.dtype,
205            compressor,
206            serde_json::json!(self.fill_value),
207        );
208        let json = meta.to_json()?;
209        let key = if self.path.is_empty() {
210            ".zarray".to_string()
211        } else {
212            format!("{}/.zarray", self.path)
213        };
214        self.store.set(&key, &json)
215    }
216
217    fn write_v3_metadata(&self) -> Result<()> {
218        let mut codecs = vec![CodecMetadata {
219            name: "bytes".to_string(),
220            configuration: Some(serde_json::json!({"endian": "little"})),
221        }];
222        if self.compressed {
223            codecs.push(CodecMetadata {
224                name: "zstd".to_string(),
225                configuration: Some(serde_json::json!({"level": 3})),
226            });
227        }
228
229        let meta = ArrayMetadataV3::new_array(
230            self.shape.clone(),
231            self.chunks.clone(),
232            self.dtype,
233            serde_json::json!(self.fill_value),
234            codecs,
235        );
236        let json = meta.to_json()?;
237        let key = if self.path.is_empty() {
238            "zarr.json".to_string()
239        } else {
240            format!("{}/zarr.json", self.path)
241        };
242        self.store.set(&key, &json)
243    }
244
245    // ── Accessors ────────────────────────────────────────────────────────
246
247    /// Array shape.
248    pub fn shape(&self) -> &[u64] {
249        &self.shape
250    }
251
252    /// Chunk shape.
253    pub fn chunk_shape(&self) -> &[u64] {
254        &self.chunks
255    }
256
257    /// Data type.
258    pub fn data_type(&self) -> DataType {
259        self.dtype
260    }
261
262    /// Zarr version.
263    pub fn version(&self) -> ZarrVersion {
264        self.version
265    }
266
267    /// Number of dimensions.
268    pub fn ndim(&self) -> usize {
269        self.shape.len()
270    }
271
272    /// Number of chunks along each dimension.
273    pub fn num_chunks(&self) -> Vec<u64> {
274        self.shape
275            .iter()
276            .zip(self.chunks.iter())
277            .map(|(&s, &c)| (s + c - 1) / c)
278            .collect()
279    }
280
281    // ── Chunk I/O ────────────────────────────────────────────────────────
282
283    /// Compute the chunk key for given chunk coordinates.
284    fn chunk_key(&self, coords: &[u64]) -> String {
285        match self.version {
286            ZarrVersion::V2 => chunk_key_v2(&self.path, coords, &self.dim_separator),
287            ZarrVersion::V3 => chunk_key_v3(&self.path, coords, &self.v3_separator),
288        }
289    }
290
291    /// Number of elements in a single chunk.
292    fn chunk_num_elements(&self) -> usize {
293        self.chunks.iter().map(|&c| c as usize).product()
294    }
295
296    /// Read a single chunk. Returns fill values if the chunk does not exist.
297    pub fn read_chunk(&self, coords: &[u64]) -> Result<Vec<f64>> {
298        let key = self.chunk_key(coords);
299        let n = self.chunk_num_elements();
300
301        if !self.store.exists(&key) {
302            return Ok(vec![self.fill_value; n]);
303        }
304
305        let raw = self.store.get(&key)?;
306        let decoded = self.pipeline.decode(&raw)?;
307        bytes_to_f64(&decoded, self.dtype, n)
308    }
309
310    /// Write a single chunk.
311    pub fn write_chunk(&self, coords: &[u64], data: &[f64]) -> Result<()> {
312        let n = self.chunk_num_elements();
313        if data.len() != n {
314            return Err(IoError::FormatError(format!(
315                "Chunk data length {} != expected {}",
316                data.len(),
317                n
318            )));
319        }
320        let raw = f64_to_bytes(data, self.dtype)?;
321        let encoded = self.pipeline.encode(&raw)?;
322        let key = self.chunk_key(coords);
323        self.store.set(&key, &encoded)
324    }
325
326    // ── Slice I/O (arbitrary selection across chunk boundaries) ──────────
327
328    /// Read a rectangular selection from the array.
329    ///
330    /// `start` and `end` are per-dimension half-open ranges `[start, end)`.
331    /// Returns data in C-order (row-major).
332    pub fn get(&self, start: &[u64], end: &[u64]) -> Result<Vec<f64>> {
333        let ndim = self.ndim();
334        if start.len() != ndim || end.len() != ndim {
335            return Err(IoError::FormatError(format!(
336                "Selection dimensionality mismatch: ndim={ndim}, start.len={}, end.len={}",
337                start.len(),
338                end.len()
339            )));
340        }
341        for d in 0..ndim {
342            if start[d] >= end[d] || end[d] > self.shape[d] {
343                return Err(IoError::FormatError(format!(
344                    "Invalid selection range in dim {d}: [{}, {}) out of [0, {})",
345                    start[d], end[d], self.shape[d]
346                )));
347            }
348        }
349
350        let sel_shape: Vec<u64> = (0..ndim).map(|d| end[d] - start[d]).collect();
351        let total: usize = sel_shape.iter().map(|&s| s as usize).product();
352        let mut result = vec![0.0f64; total];
353
354        // Iterate over all chunks that overlap the selection
355        let chunk_start: Vec<u64> = (0..ndim).map(|d| start[d] / self.chunks[d]).collect();
356        let chunk_end: Vec<u64> = (0..ndim)
357            .map(|d| (end[d] + self.chunks[d] - 1) / self.chunks[d])
358            .collect();
359
360        let mut chunk_coords = chunk_start.clone();
361        loop {
362            let chunk_data = self.read_chunk(&chunk_coords)?;
363
364            // Compute overlap between this chunk and the selection
365            let c_start: Vec<u64> = (0..ndim)
366                .map(|d| chunk_coords[d] * self.chunks[d])
367                .collect();
368
369            let overlap_start: Vec<u64> = (0..ndim).map(|d| start[d].max(c_start[d])).collect();
370            let overlap_end: Vec<u64> = (0..ndim)
371                .map(|d| end[d].min(c_start[d] + self.chunks[d]))
372                .collect();
373
374            // Copy elements from chunk into result
375            let overlap_shape: Vec<u64> = (0..ndim)
376                .map(|d| overlap_end[d] - overlap_start[d])
377                .collect();
378            let overlap_total: usize = overlap_shape.iter().map(|&s| s as usize).product();
379
380            for linear in 0..overlap_total {
381                // Convert to multi-dim index within overlap
382                let mut multi = vec![0u64; ndim];
383                let mut rem = linear;
384                for d in (0..ndim).rev() {
385                    multi[d] = (rem % overlap_shape[d] as usize) as u64;
386                    rem /= overlap_shape[d] as usize;
387                }
388
389                // Index into chunk data (C-order within chunk)
390                let chunk_idx: Vec<u64> = (0..ndim)
391                    .map(|d| overlap_start[d] + multi[d] - c_start[d])
392                    .collect();
393                let chunk_linear = c_order_index(&chunk_idx, &self.chunks);
394
395                // Index into result (C-order within selection)
396                let sel_idx: Vec<u64> = (0..ndim)
397                    .map(|d| overlap_start[d] + multi[d] - start[d])
398                    .collect();
399                let sel_linear = c_order_index(&sel_idx, &sel_shape);
400
401                result[sel_linear] = chunk_data[chunk_linear];
402            }
403
404            // Advance chunk coordinates (last dimension first)
405            if !advance_coords(&mut chunk_coords, &chunk_start, &chunk_end, ndim) {
406                break;
407            }
408        }
409
410        Ok(result)
411    }
412
413    /// Write a rectangular selection into the array.
414    ///
415    /// `start` and `end` define the half-open range per dimension.
416    /// `data` is in C-order.
417    pub fn set(&self, start: &[u64], end: &[u64], data: &[f64]) -> Result<()> {
418        let ndim = self.ndim();
419        if start.len() != ndim || end.len() != ndim {
420            return Err(IoError::FormatError(format!(
421                "Selection dimensionality mismatch: ndim={ndim}, start.len={}, end.len={}",
422                start.len(),
423                end.len()
424            )));
425        }
426        let sel_shape: Vec<u64> = (0..ndim).map(|d| end[d] - start[d]).collect();
427        let total: usize = sel_shape.iter().map(|&s| s as usize).product();
428        if data.len() != total {
429            return Err(IoError::FormatError(format!(
430                "Data length {} != selection size {}",
431                data.len(),
432                total
433            )));
434        }
435
436        let chunk_start: Vec<u64> = (0..ndim).map(|d| start[d] / self.chunks[d]).collect();
437        let chunk_end: Vec<u64> = (0..ndim)
438            .map(|d| (end[d] + self.chunks[d] - 1) / self.chunks[d])
439            .collect();
440
441        let mut chunk_coords = chunk_start.clone();
442        loop {
443            // Read existing chunk (or fill)
444            let mut chunk_data = self.read_chunk(&chunk_coords)?;
445
446            let c_start: Vec<u64> = (0..ndim)
447                .map(|d| chunk_coords[d] * self.chunks[d])
448                .collect();
449            let overlap_start: Vec<u64> = (0..ndim).map(|d| start[d].max(c_start[d])).collect();
450            let overlap_end: Vec<u64> = (0..ndim)
451                .map(|d| end[d].min(c_start[d] + self.chunks[d]))
452                .collect();
453            let overlap_shape: Vec<u64> = (0..ndim)
454                .map(|d| overlap_end[d] - overlap_start[d])
455                .collect();
456            let overlap_total: usize = overlap_shape.iter().map(|&s| s as usize).product();
457
458            for linear in 0..overlap_total {
459                let mut multi = vec![0u64; ndim];
460                let mut rem = linear;
461                for d in (0..ndim).rev() {
462                    multi[d] = (rem % overlap_shape[d] as usize) as u64;
463                    rem /= overlap_shape[d] as usize;
464                }
465
466                let chunk_idx: Vec<u64> = (0..ndim)
467                    .map(|d| overlap_start[d] + multi[d] - c_start[d])
468                    .collect();
469                let chunk_linear = c_order_index(&chunk_idx, &self.chunks);
470
471                let sel_idx: Vec<u64> = (0..ndim)
472                    .map(|d| overlap_start[d] + multi[d] - start[d])
473                    .collect();
474                let sel_linear = c_order_index(&sel_idx, &sel_shape);
475
476                chunk_data[chunk_linear] = data[sel_linear];
477            }
478
479            self.write_chunk(&chunk_coords, &chunk_data)?;
480
481            if !advance_coords(&mut chunk_coords, &chunk_start, &chunk_end, ndim) {
482                break;
483            }
484        }
485
486        Ok(())
487    }
488}
489
490// ── Helpers ──────────────────────────────────────────────────────────────────
491
492/// Compute C-order (row-major) linear index from multi-dim indices.
493fn c_order_index(indices: &[u64], shape: &[u64]) -> usize {
494    let mut linear = 0usize;
495    let mut stride = 1usize;
496    for d in (0..indices.len()).rev() {
497        linear += indices[d] as usize * stride;
498        stride *= shape[d] as usize;
499    }
500    linear
501}
502
503/// Advance multi-dimensional coordinates (last dim first). Returns false when done.
504fn advance_coords(coords: &mut [u64], start: &[u64], end: &[u64], ndim: usize) -> bool {
505    for d in (0..ndim).rev() {
506        coords[d] += 1;
507        if coords[d] < end[d] {
508            return true;
509        }
510        coords[d] = start[d];
511    }
512    false
513}
514
515/// Build a codec pipeline for the given dtype and compression setting.
516fn build_pipeline(dtype: DataType, compress: bool) -> CodecPipeline {
517    let mut pipeline = CodecPipeline::new();
518    pipeline.push(BytesCodec::new(Endian::Little, dtype.byte_size()));
519    if compress {
520        pipeline.push(ZstdCodec::default());
521    }
522    pipeline
523}
524
525/// Convert raw bytes to Vec<f64> according to the data type (little-endian).
526fn bytes_to_f64(data: &[u8], dtype: DataType, n: usize) -> Result<Vec<f64>> {
527    let elem = dtype.byte_size();
528    if data.len() < n * elem {
529        return Err(IoError::FormatError(format!(
530            "Chunk data too short: {} bytes for {} elements of {} bytes each",
531            data.len(),
532            n,
533            elem
534        )));
535    }
536    let mut out = Vec::with_capacity(n);
537    for i in 0..n {
538        let offset = i * elem;
539        let val = match dtype {
540            DataType::Bool => {
541                if data[offset] != 0 {
542                    1.0
543                } else {
544                    0.0
545                }
546            }
547            DataType::Int8 => data[offset] as i8 as f64,
548            DataType::Int16 => {
549                let b = [data[offset], data[offset + 1]];
550                i16::from_le_bytes(b) as f64
551            }
552            DataType::Int32 => {
553                let mut b = [0u8; 4];
554                b.copy_from_slice(&data[offset..offset + 4]);
555                i32::from_le_bytes(b) as f64
556            }
557            DataType::Int64 => {
558                let mut b = [0u8; 8];
559                b.copy_from_slice(&data[offset..offset + 8]);
560                i64::from_le_bytes(b) as f64
561            }
562            DataType::UInt8 => data[offset] as f64,
563            DataType::UInt16 => {
564                let b = [data[offset], data[offset + 1]];
565                u16::from_le_bytes(b) as f64
566            }
567            DataType::UInt32 => {
568                let mut b = [0u8; 4];
569                b.copy_from_slice(&data[offset..offset + 4]);
570                u32::from_le_bytes(b) as f64
571            }
572            DataType::UInt64 => {
573                let mut b = [0u8; 8];
574                b.copy_from_slice(&data[offset..offset + 8]);
575                u64::from_le_bytes(b) as f64
576            }
577            DataType::Float32 => {
578                let mut b = [0u8; 4];
579                b.copy_from_slice(&data[offset..offset + 4]);
580                f32::from_le_bytes(b) as f64
581            }
582            DataType::Float64 => {
583                let mut b = [0u8; 8];
584                b.copy_from_slice(&data[offset..offset + 8]);
585                f64::from_le_bytes(b)
586            }
587        };
588        out.push(val);
589    }
590    Ok(out)
591}
592
593/// Convert Vec<f64> to raw bytes in the target data type (little-endian).
594fn f64_to_bytes(data: &[f64], dtype: DataType) -> Result<Vec<u8>> {
595    let elem = dtype.byte_size();
596    let mut out = Vec::with_capacity(data.len() * elem);
597    for &val in data {
598        match dtype {
599            DataType::Bool => out.push(if val != 0.0 { 1 } else { 0 }),
600            DataType::Int8 => out.push(val as i8 as u8),
601            DataType::Int16 => out.extend_from_slice(&(val as i16).to_le_bytes()),
602            DataType::Int32 => out.extend_from_slice(&(val as i32).to_le_bytes()),
603            DataType::Int64 => out.extend_from_slice(&(val as i64).to_le_bytes()),
604            DataType::UInt8 => out.push(val as u8),
605            DataType::UInt16 => out.extend_from_slice(&(val as u16).to_le_bytes()),
606            DataType::UInt32 => out.extend_from_slice(&(val as u32).to_le_bytes()),
607            DataType::UInt64 => out.extend_from_slice(&(val as u64).to_le_bytes()),
608            DataType::Float32 => out.extend_from_slice(&(val as f32).to_le_bytes()),
609            DataType::Float64 => out.extend_from_slice(&val.to_le_bytes()),
610        }
611    }
612    Ok(out)
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618    use std::fs;
619
620    fn temp_store(name: &str) -> (DirectoryStore, std::path::PathBuf) {
621        let dir = std::env::temp_dir().join(format!("zarr_array_test_{name}"));
622        let _ = fs::remove_dir_all(&dir);
623        let store = DirectoryStore::open(&dir).expect("open store");
624        (store, dir)
625    }
626
627    #[test]
628    fn test_create_and_write_read_chunk_v2() {
629        let (store, dir) = temp_store("v2_chunk");
630        let arr = ZarrArray::create(
631            store,
632            "data",
633            vec![10, 20],
634            vec![5, 10],
635            DataType::Float64,
636            ZarrVersion::V2,
637        )
638        .expect("create");
639
640        assert_eq!(arr.shape(), &[10, 20]);
641        assert_eq!(arr.ndim(), 2);
642        assert_eq!(arr.num_chunks(), vec![2, 2]);
643
644        // Write a chunk
645        let chunk_data: Vec<f64> = (0..50).map(|i| i as f64).collect();
646        arr.write_chunk(&[0, 0], &chunk_data).expect("write chunk");
647
648        // Read it back
649        let read = arr.read_chunk(&[0, 0]).expect("read chunk");
650        assert_eq!(read, chunk_data);
651
652        // Read missing chunk -> fill values
653        let fill = arr.read_chunk(&[1, 1]).expect("read missing");
654        assert!(fill.iter().all(|&v| v == 0.0));
655        assert_eq!(fill.len(), 50);
656
657        let _ = fs::remove_dir_all(&dir);
658    }
659
660    #[test]
661    fn test_create_and_write_read_chunk_v3() {
662        let (store, dir) = temp_store("v3_chunk");
663        let arr = ZarrArray::create(
664            store,
665            "arr3",
666            vec![8],
667            vec![4],
668            DataType::Int32,
669            ZarrVersion::V3,
670        )
671        .expect("create");
672
673        let data: Vec<f64> = vec![10.0, 20.0, 30.0, 40.0];
674        arr.write_chunk(&[0], &data).expect("write");
675        let read = arr.read_chunk(&[0]).expect("read");
676        assert_eq!(read, data);
677
678        let _ = fs::remove_dir_all(&dir);
679    }
680
681    #[test]
682    fn test_open_existing_v2() {
683        let (store, dir) = temp_store("v2_open");
684        {
685            let arr = ZarrArray::create(
686                store,
687                "myarr",
688                vec![100],
689                vec![25],
690                DataType::Float32,
691                ZarrVersion::V2,
692            )
693            .expect("create");
694            let chunk: Vec<f64> = (0..25).map(|i| i as f64 * 0.5).collect();
695            arr.write_chunk(&[2], &chunk).expect("write");
696        }
697
698        // Re-open
699        let store2 = DirectoryStore::open(&dir).expect("reopen");
700        let arr2 = ZarrArray::open(store2, "myarr").expect("open");
701        assert_eq!(arr2.shape(), &[100]);
702        assert_eq!(arr2.data_type(), DataType::Float32);
703        let read = arr2.read_chunk(&[2]).expect("read");
704        // Float32 roundtrip loses some precision
705        assert_eq!(read.len(), 25);
706        assert!((read[0] - 0.0).abs() < 1e-6);
707        assert!((read[1] - 0.5).abs() < 1e-6);
708
709        let _ = fs::remove_dir_all(&dir);
710    }
711
712    #[test]
713    fn test_open_existing_v3() {
714        let (store, dir) = temp_store("v3_open");
715        {
716            let arr = ZarrArray::create(
717                store,
718                "v3arr",
719                vec![6, 4],
720                vec![3, 2],
721                DataType::Float64,
722                ZarrVersion::V3,
723            )
724            .expect("create");
725            let chunk: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
726            arr.write_chunk(&[0, 0], &chunk).expect("write");
727        }
728
729        let store2 = DirectoryStore::open(&dir).expect("reopen");
730        let arr2 = ZarrArray::open(store2, "v3arr").expect("open");
731        assert_eq!(arr2.shape(), &[6, 4]);
732        assert_eq!(arr2.version(), ZarrVersion::V3);
733        let read = arr2.read_chunk(&[0, 0]).expect("read");
734        assert_eq!(read, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
735
736        let _ = fs::remove_dir_all(&dir);
737    }
738
739    #[test]
740    fn test_slice_read_within_chunk() {
741        let (store, dir) = temp_store("slice_within");
742        let arr = ZarrArray::create(
743            store,
744            "s",
745            vec![10],
746            vec![5],
747            DataType::Float64,
748            ZarrVersion::V2,
749        )
750        .expect("create");
751
752        let chunk0: Vec<f64> = vec![10.0, 11.0, 12.0, 13.0, 14.0];
753        arr.write_chunk(&[0], &chunk0).expect("write");
754
755        let slice = arr.get(&[1], &[4]).expect("get");
756        assert_eq!(slice, vec![11.0, 12.0, 13.0]);
757
758        let _ = fs::remove_dir_all(&dir);
759    }
760
761    #[test]
762    fn test_slice_read_across_chunks() {
763        let (store, dir) = temp_store("slice_across");
764        let arr = ZarrArray::create(
765            store,
766            "x",
767            vec![10],
768            vec![4],
769            DataType::Float64,
770            ZarrVersion::V2,
771        )
772        .expect("create");
773
774        // chunk 0: [0,1,2,3], chunk 1: [4,5,6,7], chunk 2: [8,9,fill,fill]
775        arr.write_chunk(&[0], &[0.0, 1.0, 2.0, 3.0]).expect("w0");
776        arr.write_chunk(&[1], &[4.0, 5.0, 6.0, 7.0]).expect("w1");
777        arr.write_chunk(&[2], &[8.0, 9.0, 0.0, 0.0]).expect("w2");
778
779        let slice = arr.get(&[2], &[7]).expect("get across");
780        assert_eq!(slice, vec![2.0, 3.0, 4.0, 5.0, 6.0]);
781
782        let _ = fs::remove_dir_all(&dir);
783    }
784
785    #[test]
786    fn test_slice_write_across_chunks() {
787        let (store, dir) = temp_store("slice_write");
788        let arr = ZarrArray::create_with_options(
789            store,
790            "w",
791            vec![8],
792            vec![4],
793            DataType::Float64,
794            ZarrVersion::V2,
795            -1.0,
796            false,
797        )
798        .expect("create");
799
800        // Write across chunk boundary
801        arr.set(&[2], &[6], &[10.0, 20.0, 30.0, 40.0]).expect("set");
802
803        let all = arr.get(&[0], &[8]).expect("get all");
804        assert_eq!(all, vec![-1.0, -1.0, 10.0, 20.0, 30.0, 40.0, -1.0, -1.0]);
805
806        let _ = fs::remove_dir_all(&dir);
807    }
808
809    #[test]
810    fn test_2d_slice_across_chunks() {
811        let (store, dir) = temp_store("2d_slice");
812        let arr = ZarrArray::create(
813            store,
814            "m",
815            vec![6, 6],
816            vec![3, 3],
817            DataType::Float64,
818            ZarrVersion::V2,
819        )
820        .expect("create");
821
822        // Fill chunk (0,0): 3x3
823        let c00: Vec<f64> = (0..9).map(|i| (i + 1) as f64).collect();
824        arr.write_chunk(&[0, 0], &c00).expect("write 0,0");
825
826        // Fill chunk (0,1): 3x3
827        let c01: Vec<f64> = (10..19).map(|i| i as f64).collect();
828        arr.write_chunk(&[0, 1], &c01).expect("write 0,1");
829
830        // Read across chunks: rows 0..2, cols 1..5
831        let slice = arr.get(&[0, 1], &[2, 5]).expect("get 2d");
832        // Expected from C-order:
833        // row0: c00[0,1], c00[0,2], c01[0,0], c01[0,1] = 2,3,10,11
834        // row1: c00[1,1], c00[1,2], c01[1,0], c01[1,1] = 5,6,13,14
835        assert_eq!(slice, vec![2.0, 3.0, 10.0, 11.0, 5.0, 6.0, 13.0, 14.0]);
836
837        let _ = fs::remove_dir_all(&dir);
838    }
839
840    #[test]
841    fn test_uint8_dtype() {
842        let (store, dir) = temp_store("uint8");
843        let arr = ZarrArray::create(
844            store,
845            "u8",
846            vec![4],
847            vec![4],
848            DataType::UInt8,
849            ZarrVersion::V2,
850        )
851        .expect("create");
852
853        arr.write_chunk(&[0], &[0.0, 127.0, 200.0, 255.0])
854            .expect("write");
855        let read = arr.read_chunk(&[0]).expect("read");
856        assert_eq!(read, vec![0.0, 127.0, 200.0, 255.0]);
857
858        let _ = fs::remove_dir_all(&dir);
859    }
860
861    #[test]
862    fn test_fill_value_custom() {
863        let (store, dir) = temp_store("fill_custom");
864        let arr = ZarrArray::create_with_options(
865            store,
866            "f",
867            vec![10],
868            vec![5],
869            DataType::Float64,
870            ZarrVersion::V2,
871            f64::NAN,
872            false,
873        )
874        .expect("create");
875
876        let read = arr.read_chunk(&[0]).expect("read missing");
877        assert!(read.iter().all(|v| v.is_nan()));
878
879        let _ = fs::remove_dir_all(&dir);
880    }
881
882    #[test]
883    fn test_uncompressed_array() {
884        let (store, dir) = temp_store("no_compress");
885        let arr = ZarrArray::create_with_options(
886            store,
887            "nc",
888            vec![4],
889            vec![4],
890            DataType::Float64,
891            ZarrVersion::V2,
892            0.0,
893            false,
894        )
895        .expect("create");
896
897        let data = vec![1.0, 2.0, 3.0, 4.0];
898        arr.write_chunk(&[0], &data).expect("write");
899        let read = arr.read_chunk(&[0]).expect("read");
900        assert_eq!(read, data);
901
902        let _ = fs::remove_dir_all(&dir);
903    }
904}