1use crate::error::{IoError, Result};
40use crate::format_detect::{detect_format, DataFormat};
41use std::collections::HashMap;
42use std::path::Path;
43
44#[derive(Debug, Clone)]
50pub enum DataColumn {
51 Int32(Vec<i32>),
53 Int64(Vec<i64>),
55 Float32(Vec<f32>),
57 Float64(Vec<f64>),
59 String(Vec<String>),
61 Boolean(Vec<bool>),
63 Bytes(Vec<Vec<u8>>),
65}
66
67impl DataColumn {
68 pub fn len(&self) -> usize {
70 match self {
71 Self::Int32(v) => v.len(),
72 Self::Int64(v) => v.len(),
73 Self::Float32(v) => v.len(),
74 Self::Float64(v) => v.len(),
75 Self::String(v) => v.len(),
76 Self::Boolean(v) => v.len(),
77 Self::Bytes(v) => v.len(),
78 }
79 }
80
81 pub fn is_empty(&self) -> bool {
83 self.len() == 0
84 }
85
86 pub fn as_f64(&self) -> Vec<f64> {
88 match self {
89 Self::Int32(v) => v.iter().map(|x| *x as f64).collect(),
90 Self::Int64(v) => v.iter().map(|x| *x as f64).collect(),
91 Self::Float32(v) => v.iter().map(|x| *x as f64).collect(),
92 Self::Float64(v) => v.clone(),
93 Self::Boolean(v) => v.iter().map(|x| if *x { 1.0 } else { 0.0 }).collect(),
94 _ => Vec::new(),
95 }
96 }
97
98 pub fn as_strings(&self) -> Vec<String> {
100 match self {
101 Self::Int32(v) => v.iter().map(|x| x.to_string()).collect(),
102 Self::Int64(v) => v.iter().map(|x| x.to_string()).collect(),
103 Self::Float32(v) => v.iter().map(|x| x.to_string()).collect(),
104 Self::Float64(v) => v.iter().map(|x| x.to_string()).collect(),
105 Self::String(v) => v.clone(),
106 Self::Boolean(v) => v.iter().map(|x| x.to_string()).collect(),
107 Self::Bytes(v) => v.iter().map(|b| format!("<{} bytes>", b.len())).collect(),
108 }
109 }
110
111 pub fn type_name(&self) -> &'static str {
113 match self {
114 Self::Int32(_) => "int32",
115 Self::Int64(_) => "int64",
116 Self::Float32(_) => "float32",
117 Self::Float64(_) => "float64",
118 Self::String(_) => "string",
119 Self::Boolean(_) => "bool",
120 Self::Bytes(_) => "bytes",
121 }
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct DataTable {
128 column_names: Vec<String>,
130 columns: HashMap<String, DataColumn>,
132 metadata: HashMap<String, String>,
134 source_format: DataFormat,
136}
137
138impl DataTable {
139 pub fn new(source_format: DataFormat) -> Self {
141 Self {
142 column_names: Vec::new(),
143 columns: HashMap::new(),
144 metadata: HashMap::new(),
145 source_format,
146 }
147 }
148
149 pub fn add_column(&mut self, name: &str, data: DataColumn) {
151 if !self.columns.contains_key(name) {
152 self.column_names.push(name.to_string());
153 }
154 self.columns.insert(name.to_string(), data);
155 }
156
157 pub fn set_metadata(&mut self, key: &str, value: &str) {
159 self.metadata.insert(key.to_string(), value.to_string());
160 }
161
162 pub fn column_names(&self) -> &[String] {
164 &self.column_names
165 }
166
167 pub fn column(&self, name: &str) -> Option<&DataColumn> {
169 self.columns.get(name)
170 }
171
172 pub fn column_by_index(&self, index: usize) -> Option<&DataColumn> {
174 self.column_names
175 .get(index)
176 .and_then(|name| self.columns.get(name))
177 }
178
179 pub fn num_columns(&self) -> usize {
181 self.column_names.len()
182 }
183
184 pub fn num_rows(&self) -> usize {
186 self.column_names
187 .first()
188 .and_then(|name| self.columns.get(name))
189 .map_or(0, |col| col.len())
190 }
191
192 pub fn metadata(&self, key: &str) -> Option<&str> {
194 self.metadata.get(key).map(|s| s.as_str())
195 }
196
197 pub fn all_metadata(&self) -> &HashMap<String, String> {
199 &self.metadata
200 }
201
202 pub fn source_format(&self) -> DataFormat {
204 self.source_format
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.num_rows() == 0
210 }
211
212 pub fn summary(&self) -> String {
214 let mut lines = Vec::new();
215 lines.push(format!(
216 "DataTable: {} columns x {} rows (from {})",
217 self.num_columns(),
218 self.num_rows(),
219 self.source_format
220 ));
221 for name in &self.column_names {
222 if let Some(col) = self.columns.get(name) {
223 lines.push(format!(
224 " {}: {} ({} values)",
225 name,
226 col.type_name(),
227 col.len()
228 ));
229 }
230 }
231 lines.join("\n")
232 }
233
234 pub fn select_columns(&self, names: &[&str]) -> Result<DataTable> {
236 let mut table = DataTable::new(self.source_format);
237 for &name in names {
238 let col = self
239 .column(name)
240 .ok_or_else(|| IoError::NotFound(format!("Column '{name}' not found")))?;
241 table.add_column(name, col.clone());
242 }
243 table.metadata = self.metadata.clone();
244 Ok(table)
245 }
246
247 pub fn slice_rows(&self, start: usize, end: usize) -> DataTable {
249 let mut table = DataTable::new(self.source_format);
250 for name in &self.column_names {
251 if let Some(col) = self.columns.get(name) {
252 let sliced = slice_column(col, start, end);
253 table.add_column(name, sliced);
254 }
255 }
256 table.metadata = self.metadata.clone();
257 table
258 }
259}
260
261#[derive(Debug, Clone, Default)]
263pub struct ReadOptions {
264 pub format: Option<DataFormat>,
266 pub max_rows: Option<usize>,
268 pub columns: Option<Vec<String>>,
270 pub csv_delimiter: Option<char>,
272 pub csv_has_header: Option<bool>,
274 pub hdf5_dataset: Option<String>,
276 pub netcdf_variable: Option<String>,
278}
279
280pub fn read_data<P: AsRef<Path>>(path: P, options: Option<ReadOptions>) -> Result<DataTable> {
289 let path = path.as_ref();
290 let opts = options.unwrap_or_default();
291
292 let format = if let Some(fmt) = opts.format {
293 fmt
294 } else {
295 detect_format(path)?
296 };
297
298 match format {
299 DataFormat::Csv => read_csv_to_table(path, &opts),
300 DataFormat::ArrowIpc => read_arrow_to_table(path, &opts),
301 DataFormat::Json => read_json_to_table(path, &opts),
302 DataFormat::Wav => read_wav_to_table(path, &opts),
303 DataFormat::Npy => read_npy_to_table(path, &opts),
304 DataFormat::MatrixMarket => read_mtx_to_table(path, &opts),
305 DataFormat::Hdf5 => read_hdf5_to_table(path, &opts),
306 DataFormat::NetCdf => read_netcdf_to_table(path, &opts),
307 _ => Err(IoError::UnsupportedFormat(format!(
308 "Universal reader does not support {} format yet",
309 format.name()
310 ))),
311 }
312}
313
314pub struct StreamingReader {
320 chunks: Vec<DataTable>,
322 current: usize,
324}
325
326impl StreamingReader {
327 pub fn open<P: AsRef<Path>>(
329 path: P,
330 chunk_size: usize,
331 options: Option<ReadOptions>,
332 ) -> Result<Self> {
333 let full = read_data(path, options)?;
335 let total_rows = full.num_rows();
336
337 let mut chunks = Vec::new();
338 let mut offset = 0;
339 while offset < total_rows {
340 let end = (offset + chunk_size).min(total_rows);
341 chunks.push(full.slice_rows(offset, end));
342 offset = end;
343 }
344
345 if chunks.is_empty() {
346 chunks.push(full);
347 }
348
349 Ok(Self { chunks, current: 0 })
350 }
351
352 pub fn next_chunk(&mut self) -> Option<&DataTable> {
354 if self.current < self.chunks.len() {
355 let chunk = &self.chunks[self.current];
356 self.current += 1;
357 Some(chunk)
358 } else {
359 None
360 }
361 }
362
363 pub fn reset(&mut self) {
365 self.current = 0;
366 }
367
368 pub fn num_chunks(&self) -> usize {
370 self.chunks.len()
371 }
372
373 pub fn total_rows(&self) -> usize {
375 self.chunks.iter().map(|c| c.num_rows()).sum()
376 }
377}
378
379fn read_csv_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
385 let config = crate::csv::CsvReaderConfig {
386 has_header: opts.csv_has_header.unwrap_or(true),
387 delimiter: opts.csv_delimiter.unwrap_or(','),
388 ..Default::default()
389 };
390
391 let (headers, data) = crate::csv::read_csv(path, Some(config))?;
392
393 let mut table = DataTable::new(DataFormat::Csv);
394 table.set_metadata("source_file", &path.display().to_string());
395
396 if headers.is_empty() {
397 return Ok(table);
398 }
399
400 let nrows = data.nrows();
401 let ncols = data.ncols();
402
403 for (col_idx, header) in headers.iter().enumerate() {
405 if col_idx >= ncols {
406 break;
407 }
408
409 let col_name = if header.is_empty() {
410 format!("column_{col_idx}")
411 } else {
412 header.clone()
413 };
414
415 if let Some(ref wanted) = opts.columns {
417 if !wanted.iter().any(|w| w == &col_name) {
418 continue;
419 }
420 }
421
422 let max = opts.max_rows.unwrap_or(usize::MAX);
423 let row_count = nrows.min(max);
424
425 let values: Vec<&str> = (0..row_count)
426 .map(|r| data[[r, col_idx]].as_str())
427 .collect();
428
429 let as_int: std::result::Result<Vec<i64>, _> = values
431 .iter()
432 .map(|s: &&str| s.trim().parse::<i64>())
433 .collect();
434
435 if let Ok(ints) = as_int {
436 table.add_column(&col_name, DataColumn::Int64(ints));
437 continue;
438 }
439
440 let as_float: std::result::Result<Vec<f64>, _> = values
442 .iter()
443 .map(|s: &&str| s.trim().parse::<f64>())
444 .collect();
445
446 if let Ok(floats) = as_float {
447 table.add_column(&col_name, DataColumn::Float64(floats));
448 continue;
449 }
450
451 let as_bool: std::result::Result<Vec<bool>, _> = values
453 .iter()
454 .map(|s: &&str| match s.trim().to_lowercase().as_str() {
455 "true" | "1" | "yes" => Ok(true),
456 "false" | "0" | "no" => Ok(false),
457 _ => Err(()),
458 })
459 .collect();
460
461 if let Ok(bools) = as_bool {
462 table.add_column(&col_name, DataColumn::Boolean(bools));
463 continue;
464 }
465
466 let strings: Vec<String> = values.iter().map(|s: &&str| s.to_string()).collect();
468 table.add_column(&col_name, DataColumn::String(strings));
469 }
470
471 Ok(table)
472}
473
474fn read_arrow_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
476 let (schema, batches) = crate::arrow_ipc::read_arrow_ipc_file(path)?;
477
478 let mut table = DataTable::new(DataFormat::ArrowIpc);
479 table.set_metadata("source_file", &path.display().to_string());
480
481 for (key, val) in &schema.metadata {
482 table.set_metadata(key, val);
483 }
484
485 for (idx, field) in schema.fields.iter().enumerate() {
487 if let Some(ref wanted) = opts.columns {
488 if !wanted.iter().any(|w| w == &field.name) {
489 continue;
490 }
491 }
492
493 let col = merge_arrow_columns(&batches, idx, &field.dtype, opts.max_rows)?;
494 table.add_column(&field.name, col);
495 }
496
497 Ok(table)
498}
499
500fn merge_arrow_columns(
502 batches: &[crate::arrow_ipc::RecordBatch],
503 col_idx: usize,
504 dtype: &crate::arrow_ipc::ArrowDataType,
505 max_rows: Option<usize>,
506) -> Result<DataColumn> {
507 let max = max_rows.unwrap_or(usize::MAX);
508
509 match dtype {
510 crate::arrow_ipc::ArrowDataType::Int32 => {
511 let mut values = Vec::new();
512 for batch in batches {
513 if values.len() >= max {
514 break;
515 }
516 if let Some(crate::arrow_ipc::ArrowColumn::Int32(v)) = batch.column(col_idx) {
517 let remaining = max - values.len();
518 values.extend_from_slice(&v[..v.len().min(remaining)]);
519 }
520 }
521 Ok(DataColumn::Int32(values))
522 }
523 crate::arrow_ipc::ArrowDataType::Int64 => {
524 let mut values = Vec::new();
525 for batch in batches {
526 if values.len() >= max {
527 break;
528 }
529 if let Some(crate::arrow_ipc::ArrowColumn::Int64(v)) = batch.column(col_idx) {
530 let remaining = max - values.len();
531 values.extend_from_slice(&v[..v.len().min(remaining)]);
532 }
533 }
534 Ok(DataColumn::Int64(values))
535 }
536 crate::arrow_ipc::ArrowDataType::Float32 => {
537 let mut values = Vec::new();
538 for batch in batches {
539 if values.len() >= max {
540 break;
541 }
542 if let Some(crate::arrow_ipc::ArrowColumn::Float32(v)) = batch.column(col_idx) {
543 let remaining = max - values.len();
544 values.extend_from_slice(&v[..v.len().min(remaining)]);
545 }
546 }
547 Ok(DataColumn::Float32(values))
548 }
549 crate::arrow_ipc::ArrowDataType::Float64 => {
550 let mut values = Vec::new();
551 for batch in batches {
552 if values.len() >= max {
553 break;
554 }
555 if let Some(crate::arrow_ipc::ArrowColumn::Float64(v)) = batch.column(col_idx) {
556 let remaining = max - values.len();
557 values.extend_from_slice(&v[..v.len().min(remaining)]);
558 }
559 }
560 Ok(DataColumn::Float64(values))
561 }
562 crate::arrow_ipc::ArrowDataType::Utf8 => {
563 let mut values = Vec::new();
564 for batch in batches {
565 if values.len() >= max {
566 break;
567 }
568 if let Some(crate::arrow_ipc::ArrowColumn::Utf8(v)) = batch.column(col_idx) {
569 let remaining = max - values.len();
570 values.extend_from_slice(&v[..v.len().min(remaining)]);
571 }
572 }
573 Ok(DataColumn::String(values))
574 }
575 crate::arrow_ipc::ArrowDataType::Boolean => {
576 let mut values = Vec::new();
577 for batch in batches {
578 if values.len() >= max {
579 break;
580 }
581 if let Some(crate::arrow_ipc::ArrowColumn::Boolean(v)) = batch.column(col_idx) {
582 let remaining = max - values.len();
583 values.extend_from_slice(&v[..v.len().min(remaining)]);
584 }
585 }
586 Ok(DataColumn::Boolean(values))
587 }
588 }
589}
590
591fn read_json_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
593 let content = std::fs::read_to_string(path)
594 .map_err(|e| IoError::FileError(format!("Cannot read '{}': {e}", path.display())))?;
595
596 let parsed: serde_json::Value = serde_json::from_str(&content)
597 .map_err(|e| IoError::FormatError(format!("JSON parse error: {e}")))?;
598
599 let mut table = DataTable::new(DataFormat::Json);
600 table.set_metadata("source_file", &path.display().to_string());
601
602 let arr = match &parsed {
603 serde_json::Value::Array(a) => a,
604 serde_json::Value::Object(_) => {
605 return read_json_single_object(&parsed, &mut table, opts);
607 }
608 _ => {
609 return Err(IoError::FormatError(
610 "Expected JSON array or object".to_string(),
611 ));
612 }
613 };
614
615 if arr.is_empty() {
616 return Ok(table);
617 }
618
619 let field_names: Vec<String> = if let Some(serde_json::Value::Object(obj)) = arr.first() {
621 obj.keys().cloned().collect()
622 } else {
623 let values: Vec<f64> = arr
625 .iter()
626 .take(opts.max_rows.unwrap_or(usize::MAX))
627 .filter_map(|v| v.as_f64())
628 .collect();
629 table.add_column("value", DataColumn::Float64(values));
630 return Ok(table);
631 };
632
633 let max = opts.max_rows.unwrap_or(usize::MAX);
634
635 for field_name in &field_names {
636 if let Some(ref wanted) = opts.columns {
637 if !wanted.iter().any(|w| w == field_name) {
638 continue;
639 }
640 }
641
642 let values: Vec<&serde_json::Value> = arr
644 .iter()
645 .take(max)
646 .filter_map(|item| item.as_object().and_then(|obj| obj.get(field_name)))
647 .collect();
648
649 let all_int = values
651 .iter()
652 .all(|v| v.is_i64() || v.is_u64() || v.is_null());
653 let all_float = values.iter().all(|v| v.is_number() || v.is_null());
654 let all_bool = values.iter().all(|v| v.is_boolean() || v.is_null());
655
656 if all_bool && !values.is_empty() {
657 let bools: Vec<bool> = values
658 .iter()
659 .map(|v| v.as_bool().unwrap_or(false))
660 .collect();
661 table.add_column(field_name, DataColumn::Boolean(bools));
662 } else if all_int && !values.is_empty() {
663 let ints: Vec<i64> = values.iter().map(|v| v.as_i64().unwrap_or(0)).collect();
664 table.add_column(field_name, DataColumn::Int64(ints));
665 } else if all_float && !values.is_empty() {
666 let floats: Vec<f64> = values.iter().map(|v| v.as_f64().unwrap_or(0.0)).collect();
667 table.add_column(field_name, DataColumn::Float64(floats));
668 } else {
669 let strings: Vec<String> = values
670 .iter()
671 .map(|v| match v {
672 serde_json::Value::String(s) => s.clone(),
673 other => other.to_string(),
674 })
675 .collect();
676 table.add_column(field_name, DataColumn::String(strings));
677 }
678 }
679
680 Ok(table)
681}
682
683fn read_json_single_object(
685 obj: &serde_json::Value,
686 table: &mut DataTable,
687 _opts: &ReadOptions,
688) -> Result<DataTable> {
689 if let serde_json::Value::Object(map) = obj {
690 for (key, value) in map {
691 match value {
692 serde_json::Value::Number(n) => {
693 if let Some(i) = n.as_i64() {
694 table.add_column(key, DataColumn::Int64(vec![i]));
695 } else if let Some(f) = n.as_f64() {
696 table.add_column(key, DataColumn::Float64(vec![f]));
697 }
698 }
699 serde_json::Value::Bool(b) => {
700 table.add_column(key, DataColumn::Boolean(vec![*b]));
701 }
702 serde_json::Value::String(s) => {
703 table.add_column(key, DataColumn::String(vec![s.clone()]));
704 }
705 _ => {
706 table.add_column(key, DataColumn::String(vec![value.to_string()]));
707 }
708 }
709 }
710 }
711 Ok(table.clone())
712}
713
714fn read_wav_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
716 let (header, data) = crate::wavfile::read_wav(path)?;
717
718 let mut table = DataTable::new(DataFormat::Wav);
719 table.set_metadata("source_file", &path.display().to_string());
720 table.set_metadata("sample_rate", &header.sample_rate.to_string());
721 table.set_metadata("channels", &header.channels.to_string());
722 table.set_metadata("bits_per_sample", &header.bits_per_sample.to_string());
723
724 let max = opts.max_rows.unwrap_or(usize::MAX);
725
726 let flat: Vec<f64> = data.iter().take(max).map(|&s| s as f64).collect();
728 table.add_column("samples", DataColumn::Float64(flat));
729
730 Ok(table)
731}
732
733fn read_npy_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
735 let arr = crate::npy::read_npy(path)?;
736
737 let mut table = DataTable::new(DataFormat::Npy);
738 table.set_metadata("source_file", &path.display().to_string());
739 table.set_metadata("dtype", &format!("{:?}", arr.dtype()));
740 table.set_metadata("shape", &format!("{:?}", arr.shape()));
741
742 let max = opts.max_rows.unwrap_or(usize::MAX);
743
744 match &arr {
746 crate::npy::NpyArray::Float64 { data, .. } => {
747 let values: Vec<f64> = data.iter().take(max).copied().collect();
748 table.add_column("data", DataColumn::Float64(values));
749 }
750 crate::npy::NpyArray::Float32 { data, .. } => {
751 let values: Vec<f32> = data.iter().take(max).copied().collect();
752 table.add_column("data", DataColumn::Float32(values));
753 }
754 crate::npy::NpyArray::Int32 { data, .. } => {
755 let values: Vec<i32> = data.iter().take(max).copied().collect();
756 table.add_column("data", DataColumn::Int32(values));
757 }
758 crate::npy::NpyArray::Int64 { data, .. } => {
759 let values: Vec<i64> = data.iter().take(max).copied().collect();
760 table.add_column("data", DataColumn::Int64(values));
761 }
762 }
763
764 Ok(table)
765}
766
767fn read_mtx_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
769 let mm = crate::matrix_market::read_sparse_matrix(path)?;
770
771 let mut table = DataTable::new(DataFormat::MatrixMarket);
772 table.set_metadata("source_file", &path.display().to_string());
773 table.set_metadata("matrix_rows", &mm.rows.to_string());
774 table.set_metadata("matrix_cols", &mm.cols.to_string());
775 table.set_metadata("nnz", &mm.entries.len().to_string());
776
777 let max = opts.max_rows.unwrap_or(usize::MAX);
778
779 let mut row_indices = Vec::new();
781 let mut col_indices = Vec::new();
782 let mut vals = Vec::new();
783
784 for (i, entry) in mm.entries.iter().enumerate() {
785 if i >= max {
786 break;
787 }
788 row_indices.push(entry.row as i64);
789 col_indices.push(entry.col as i64);
790 vals.push(entry.value);
791 }
792
793 table.add_column("row", DataColumn::Int64(row_indices));
794 table.add_column("col", DataColumn::Int64(col_indices));
795 table.add_column("value", DataColumn::Float64(vals));
796
797 Ok(table)
798}
799
800fn read_hdf5_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
812 let file = oxih5::File::open(path)
813 .map_err(|e| IoError::FormatError(format!("Failed to open HDF5 file: {e}")))?;
814
815 let mut table = DataTable::new(DataFormat::Hdf5);
816 table.set_metadata("source_file", &path.display().to_string());
817 let info = file
818 .info()
819 .map_err(|e| IoError::FormatError(format!("Failed to read HDF5 superblock: {e}")))?;
820 table.set_metadata("superblock_version", &info.superblock_version.to_string());
821
822 if let Some(ref ds_path) = opts.hdf5_dataset {
824 let dataset = file.dataset(ds_path).map_err(|e| {
825 IoError::FormatError(format!("Failed to read dataset '{ds_path}': {e}"))
826 })?;
827 let name = ds_path
830 .split('/')
831 .rfind(|s| !s.is_empty())
832 .unwrap_or(ds_path)
833 .to_string();
834 table.add_column(&name, hdf5_dataset_to_column(&file, ds_path, &dataset)?);
835 table.set_metadata("dataset_path", ds_path);
836 table.set_metadata("shape", &format!("{:?}", dataset.shape));
837
838 let views = file.attr_views(ds_path).map_err(|e| {
839 IoError::FormatError(format!(
840 "Failed to read attributes of dataset '{ds_path}': {e}"
841 ))
842 })?;
843 for view in &views {
844 table.set_metadata(
845 &format!("attr_{}", view.name()),
846 &hdf5_attr_to_string(view, ds_path),
847 );
848 }
849 return Ok(table);
850 }
851
852 let root = file
858 .root()
859 .map_err(|e| IoError::FormatError(format!("Failed to open HDF5 root group: {e}")))?;
860 collect_hdf5_datasets(&file, &root, "", &mut table)?;
861
862 Ok(table)
863}
864
865fn collect_hdf5_datasets(
867 file: &oxih5::File,
868 group: &oxih5::Group,
869 prefix: &str,
870 table: &mut DataTable,
871) -> Result<()> {
872 let names = group.datasets().map_err(|e| {
873 IoError::FormatError(format!("Failed to list datasets of '{prefix}/': {e}"))
874 })?;
875 for name in names {
876 let full_path = format!("{prefix}/{name}");
877 if let Ok(dataset) = group.dataset(&name) {
880 if let Ok(col) = hdf5_dataset_to_column(file, &full_path, &dataset) {
881 table.add_column(&full_path, col);
882 }
883 }
884 }
885
886 let group_names = group
887 .groups()
888 .map_err(|e| IoError::FormatError(format!("Failed to list groups of '{prefix}/': {e}")))?;
889 for name in group_names {
890 let full_path = format!("{prefix}/{name}");
891 let child = group.group(&name).map_err(|e| {
892 IoError::FormatError(format!("Failed to open group '{full_path}': {e}"))
893 })?;
894 collect_hdf5_datasets(file, &child, &full_path, table)?;
895 }
896
897 Ok(())
898}
899
900fn hdf5_dataset_to_column(
910 file: &oxih5::File,
911 path: &str,
912 dataset: &oxih5::Dataset,
913) -> Result<DataColumn> {
914 use crate::hdf5::convert::{dataset_to_f64, dataset_to_i64, is_floating, is_integral};
915 use oxih5::Dtype;
916
917 match &dataset.dtype {
918 Dtype::String { .. } => {
922 let strings = file.dataset_strings(path).map_err(|e| {
923 IoError::FormatError(format!("Failed to read string dataset '{path}': {e}"))
924 })?;
925 Ok(DataColumn::String(strings))
926 }
927 dtype if is_floating(dtype) => {
928 let values = dataset_to_f64(dataset)?;
929 if matches!(dtype, Dtype::Float { size: 8, .. }) {
930 Ok(DataColumn::Float64(values))
931 } else {
932 Ok(DataColumn::Float32(
934 values.into_iter().map(|v| v as f32).collect(),
935 ))
936 }
937 }
938 dtype if is_integral(dtype) => {
939 let values = dataset_to_i64(dataset)?;
940 if hdf5_fits_i32(dtype) {
941 Ok(DataColumn::Int32(
944 values.into_iter().map(|v| v as i32).collect(),
945 ))
946 } else {
947 Ok(DataColumn::Int64(values))
948 }
949 }
950 _ => Ok(DataColumn::Bytes(vec![dataset.data.clone()])),
953 }
954}
955
956fn hdf5_fits_i32(dtype: &oxih5::Dtype) -> bool {
961 use oxih5::Dtype;
962 match dtype {
963 Dtype::Int { size, signed, .. } => {
964 if *signed {
965 matches!(size, 1 | 2 | 4)
966 } else {
967 matches!(size, 1 | 2)
968 }
969 }
970 Dtype::Bitfield { size, .. } => matches!(size, 1 | 2),
971 Dtype::Enum { base, .. } => hdf5_fits_i32(base),
972 _ => false,
973 }
974}
975
976fn hdf5_attr_to_string(view: &oxih5::AttrView<'_>, path: &str) -> String {
985 use crate::hdf5::convert::{dataset_to_f64, dataset_to_i64, is_floating, is_integral};
986
987 if let Ok(strings) = view.as_strings() {
988 return strings.join(", ");
989 }
990
991 let dtype = view.dtype();
994 let decoded = if is_floating(dtype) {
995 crate::hdf5::HDF5File::attribute_as_dataset(view, path)
996 .and_then(|ds| dataset_to_f64(&ds))
997 .map(|v| join_display(&v))
998 } else if is_integral(dtype) {
999 crate::hdf5::HDF5File::attribute_as_dataset(view, path)
1000 .and_then(|ds| dataset_to_i64(&ds))
1001 .map(|v| join_display(&v))
1002 } else {
1003 return format!("<{dtype}>");
1004 };
1005
1006 decoded.unwrap_or_else(|_| format!("<{dtype}>"))
1007}
1008
1009fn join_display<T: std::fmt::Display>(values: &[T]) -> String {
1011 values
1012 .iter()
1013 .map(T::to_string)
1014 .collect::<Vec<_>>()
1015 .join(", ")
1016}
1017
1018fn read_netcdf_to_table(path: &Path, opts: &ReadOptions) -> Result<DataTable> {
1020 let nc = crate::netcdf_lite::NcFile::read_from_file(path)?;
1021
1022 let mut table = DataTable::new(DataFormat::NetCdf);
1023 table.set_metadata("source_file", &path.display().to_string());
1024
1025 let dims = nc.dimensions();
1026 let num_records = nc.num_records();
1027
1028 if let Some(ref var_name) = opts.netcdf_variable {
1030 let var = nc.variable(var_name)?;
1031 let values = var.as_f64(dims, num_records)?;
1032 let max = opts.max_rows.unwrap_or(usize::MAX);
1033 let truncated: Vec<f64> = values.into_iter().take(max).collect();
1034 table.add_column(var_name, DataColumn::Float64(truncated));
1035 return Ok(table);
1036 }
1037
1038 let var_names: Vec<String> = nc.variable_names().iter().map(|s| s.to_string()).collect();
1040 for var_name in &var_names {
1041 if let Some(ref wanted) = opts.columns {
1042 if !wanted.iter().any(|w| w == var_name) {
1043 continue;
1044 }
1045 }
1046
1047 if let Ok(var) = nc.variable(var_name) {
1048 if let Ok(values) = var.as_f64(dims, num_records) {
1049 let max = opts.max_rows.unwrap_or(usize::MAX);
1050 let truncated: Vec<f64> = values.into_iter().take(max).collect();
1051 table.add_column(var_name, DataColumn::Float64(truncated));
1052 }
1053 }
1054 }
1055
1056 Ok(table)
1057}
1058
1059fn slice_column(col: &DataColumn, start: usize, end: usize) -> DataColumn {
1065 let s = start;
1066 let e = end;
1067 match col {
1068 DataColumn::Int32(v) => DataColumn::Int32(v[s.min(v.len())..e.min(v.len())].to_vec()),
1069 DataColumn::Int64(v) => DataColumn::Int64(v[s.min(v.len())..e.min(v.len())].to_vec()),
1070 DataColumn::Float32(v) => DataColumn::Float32(v[s.min(v.len())..e.min(v.len())].to_vec()),
1071 DataColumn::Float64(v) => DataColumn::Float64(v[s.min(v.len())..e.min(v.len())].to_vec()),
1072 DataColumn::String(v) => DataColumn::String(v[s.min(v.len())..e.min(v.len())].to_vec()),
1073 DataColumn::Boolean(v) => DataColumn::Boolean(v[s.min(v.len())..e.min(v.len())].to_vec()),
1074 DataColumn::Bytes(v) => DataColumn::Bytes(v[s.min(v.len())..e.min(v.len())].to_vec()),
1075 }
1076}
1077
1078#[cfg(test)]
1083mod tests {
1084 use super::*;
1085
1086 #[test]
1087 fn test_data_column_len() {
1088 assert_eq!(DataColumn::Int32(vec![1, 2, 3]).len(), 3);
1089 assert_eq!(DataColumn::Float64(vec![]).len(), 0);
1090 assert!(DataColumn::String(vec![]).is_empty());
1091 }
1092
1093 #[test]
1094 fn test_data_column_as_f64() {
1095 let col = DataColumn::Int32(vec![1, 2, 3]);
1096 let f = col.as_f64();
1097 assert_eq!(f, vec![1.0, 2.0, 3.0]);
1098
1099 let col = DataColumn::Boolean(vec![true, false]);
1100 let f = col.as_f64();
1101 assert_eq!(f, vec![1.0, 0.0]);
1102
1103 let col = DataColumn::String(vec!["a".to_string()]);
1104 assert!(col.as_f64().is_empty());
1105 }
1106
1107 #[test]
1108 fn test_data_column_as_strings() {
1109 let col = DataColumn::Int32(vec![42]);
1110 assert_eq!(col.as_strings(), vec!["42"]);
1111
1112 let col = DataColumn::Boolean(vec![true]);
1113 assert_eq!(col.as_strings(), vec!["true"]);
1114 }
1115
1116 #[test]
1117 fn test_data_column_type_name() {
1118 assert_eq!(DataColumn::Int32(vec![]).type_name(), "int32");
1119 assert_eq!(DataColumn::Float64(vec![]).type_name(), "float64");
1120 assert_eq!(DataColumn::String(vec![]).type_name(), "string");
1121 assert_eq!(DataColumn::Boolean(vec![]).type_name(), "bool");
1122 }
1123
1124 #[test]
1125 fn test_data_table_basic() {
1126 let mut table = DataTable::new(DataFormat::Csv);
1127 table.add_column("x", DataColumn::Float64(vec![1.0, 2.0, 3.0]));
1128 table.add_column("y", DataColumn::Int32(vec![10, 20, 30]));
1129
1130 assert_eq!(table.num_columns(), 2);
1131 assert_eq!(table.num_rows(), 3);
1132 assert!(!table.is_empty());
1133 assert_eq!(table.source_format(), DataFormat::Csv);
1134 assert_eq!(table.column_names(), &["x", "y"]);
1135 }
1136
1137 #[test]
1138 fn test_data_table_column_access() {
1139 let mut table = DataTable::new(DataFormat::Json);
1140 table.add_column("a", DataColumn::Int64(vec![1, 2]));
1141 table.add_column(
1142 "b",
1143 DataColumn::String(vec!["x".to_string(), "y".to_string()]),
1144 );
1145
1146 assert!(table.column("a").is_some());
1147 assert!(table.column("c").is_none());
1148 assert!(table.column_by_index(0).is_some());
1149 assert!(table.column_by_index(5).is_none());
1150 }
1151
1152 #[test]
1153 fn test_data_table_metadata() {
1154 let mut table = DataTable::new(DataFormat::Hdf5);
1155 table.set_metadata("key1", "value1");
1156 table.set_metadata("key2", "value2");
1157
1158 assert_eq!(table.metadata("key1"), Some("value1"));
1159 assert_eq!(table.metadata("key2"), Some("value2"));
1160 assert_eq!(table.metadata("key3"), None);
1161 assert_eq!(table.all_metadata().len(), 2);
1162 }
1163
1164 #[test]
1165 fn test_data_table_select_columns() {
1166 let mut table = DataTable::new(DataFormat::Csv);
1167 table.add_column("a", DataColumn::Int32(vec![1]));
1168 table.add_column("b", DataColumn::Int32(vec![2]));
1169 table.add_column("c", DataColumn::Int32(vec![3]));
1170
1171 let selected = table.select_columns(&["a", "c"]).expect("select");
1172 assert_eq!(selected.num_columns(), 2);
1173 assert_eq!(selected.column_names(), &["a", "c"]);
1174
1175 let err = table.select_columns(&["nonexistent"]);
1176 assert!(err.is_err());
1177 }
1178
1179 #[test]
1180 fn test_data_table_slice_rows() {
1181 let mut table = DataTable::new(DataFormat::Csv);
1182 table.add_column("x", DataColumn::Float64(vec![1.0, 2.0, 3.0, 4.0, 5.0]));
1183
1184 let sliced = table.slice_rows(1, 4);
1185 assert_eq!(sliced.num_rows(), 3);
1186 if let Some(DataColumn::Float64(v)) = sliced.column("x") {
1187 assert_eq!(v, &[2.0, 3.0, 4.0]);
1188 }
1189 }
1190
1191 #[test]
1192 fn test_data_table_summary() {
1193 let mut table = DataTable::new(DataFormat::ArrowIpc);
1194 table.add_column("temp", DataColumn::Float64(vec![20.5, 21.0]));
1195 let summary = table.summary();
1196 assert!(summary.contains("1 columns"));
1197 assert!(summary.contains("2 rows"));
1198 assert!(summary.contains("Arrow IPC"));
1199 }
1200
1201 #[test]
1202 fn test_data_table_empty() {
1203 let table = DataTable::new(DataFormat::Unknown);
1204 assert!(table.is_empty());
1205 assert_eq!(table.num_rows(), 0);
1206 assert_eq!(table.num_columns(), 0);
1207 }
1208
1209 #[test]
1210 fn test_read_options_default() {
1211 let opts = ReadOptions::default();
1212 assert!(opts.format.is_none());
1213 assert!(opts.max_rows.is_none());
1214 assert!(opts.columns.is_none());
1215 assert!(opts.csv_delimiter.is_none());
1216 }
1217
1218 #[test]
1219 fn test_slice_column_bounds() {
1220 let col = DataColumn::Int32(vec![1, 2, 3]);
1221 let sliced = slice_column(&col, 0, 100);
1222 if let DataColumn::Int32(v) = sliced {
1223 assert_eq!(v, vec![1, 2, 3]);
1224 }
1225 }
1226
1227 #[test]
1228 fn test_csv_roundtrip_via_universal() {
1229 let dir = std::env::temp_dir();
1230 let path = dir.join("universal_test.csv");
1231
1232 std::fs::write(&path, "x,y,name\n1,1.1,a\n2,2.2,b\n3,3.3,c\n").expect("write csv");
1234
1235 let table = read_data(&path, None).expect("read");
1236 assert_eq!(table.source_format(), DataFormat::Csv);
1237 assert_eq!(table.num_rows(), 3);
1238 assert!(table.column("x").is_some());
1239 assert!(table.column("y").is_some());
1240 assert!(table.column("name").is_some());
1241
1242 let _ = std::fs::remove_file(&path);
1243 }
1244
1245 #[test]
1246 fn test_json_roundtrip_via_universal() {
1247 let dir = std::env::temp_dir();
1248 let path = dir.join("universal_test.json");
1249
1250 std::fs::write(
1251 &path,
1252 r#"[{"id": 1, "val": 3.14, "ok": true}, {"id": 2, "val": 2.72, "ok": false}]"#,
1253 )
1254 .expect("write json");
1255
1256 let table = read_data(&path, None).expect("read");
1257 assert_eq!(table.source_format(), DataFormat::Json);
1258 assert_eq!(table.num_rows(), 2);
1259
1260 let _ = std::fs::remove_file(&path);
1261 }
1262
1263 #[test]
1264 fn test_arrow_roundtrip_via_universal() {
1265 let dir = std::env::temp_dir();
1266 let path = dir.join("universal_test.arrow");
1267
1268 let schema = crate::arrow_ipc::ArrowSchema::new(vec![crate::arrow_ipc::ArrowField::new(
1269 "x",
1270 crate::arrow_ipc::ArrowDataType::Float64,
1271 )]);
1272 let batch = crate::arrow_ipc::RecordBatch::new(
1273 schema.clone(),
1274 vec![crate::arrow_ipc::ArrowColumn::Float64(vec![1.0, 2.0])],
1275 )
1276 .expect("batch");
1277
1278 crate::arrow_ipc::write_arrow_ipc_file(&path, &schema, &[batch]).expect("write");
1279
1280 let table = read_data(&path, None).expect("read");
1281 assert_eq!(table.source_format(), DataFormat::ArrowIpc);
1282 assert_eq!(table.num_rows(), 2);
1283
1284 let _ = std::fs::remove_file(&path);
1285 }
1286
1287 #[test]
1288 fn test_read_with_max_rows() {
1289 let dir = std::env::temp_dir();
1290 let path = dir.join("universal_maxrows.csv");
1291
1292 std::fs::write(&path, "x\n1\n2\n3\n4\n5\n").expect("write");
1293
1294 let opts = ReadOptions {
1295 max_rows: Some(2),
1296 ..Default::default()
1297 };
1298 let table = read_data(&path, Some(opts)).expect("read");
1299 assert_eq!(table.num_rows(), 2);
1300
1301 let _ = std::fs::remove_file(&path);
1302 }
1303
1304 #[test]
1305 fn test_read_unsupported_format() {
1306 let dir = std::env::temp_dir();
1307 let path = dir.join("test_file.unsupported_ext");
1308
1309 std::fs::write(&path, [0x00, 0x01, 0x02]).expect("write");
1310
1311 let opts = ReadOptions {
1312 format: Some(DataFormat::Fits),
1313 ..Default::default()
1314 };
1315 let result = read_data(&path, Some(opts));
1316 assert!(result.is_err());
1317
1318 let _ = std::fs::remove_file(&path);
1319 }
1320
1321 #[test]
1322 fn test_streaming_reader() {
1323 let dir = std::env::temp_dir();
1324 let path = dir.join("streaming_test.csv");
1325
1326 std::fs::write(&path, "x\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n").expect("write");
1327
1328 let mut reader = StreamingReader::open(&path, 3, None).expect("open");
1329 assert_eq!(reader.total_rows(), 10);
1330 assert!(reader.num_chunks() >= 3);
1331
1332 let first_row_count = {
1333 let first = reader.next_chunk().expect("chunk1");
1334 assert!(first.num_rows() <= 3);
1335 first.num_rows()
1336 };
1337
1338 reader.reset();
1339 let first_again = reader.next_chunk().expect("chunk1 again");
1340 assert_eq!(first_again.num_rows(), first_row_count);
1341
1342 let _ = std::fs::remove_file(&path);
1343 }
1344
1345 #[test]
1346 fn test_data_column_bytes() {
1347 let col = DataColumn::Bytes(vec![vec![1, 2, 3], vec![4, 5]]);
1348 assert_eq!(col.len(), 2);
1349 assert_eq!(col.type_name(), "bytes");
1350 assert!(col.as_f64().is_empty());
1351 let s = col.as_strings();
1352 assert!(s[0].contains("3 bytes"));
1353 }
1354}