use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::sample::{Sample, Tensor};
use crate::source::{Source, SourceIterator};
pub struct CsvSource {
path: PathBuf,
has_headers: bool,
delimiter: u8,
}
impl CsvSource {
pub fn new(path: impl AsRef<Path>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
has_headers: true,
delimiter: b',',
}
}
pub fn headers(mut self, has_headers: bool) -> Self {
self.has_headers = has_headers;
self
}
pub fn delimiter(mut self, delimiter: u8) -> Self {
self.delimiter = delimiter;
self
}
}
impl Source for CsvSource {
fn open(&self) -> Result<Box<dyn SourceIterator>> {
let mut reader = csv::ReaderBuilder::new()
.has_headers(self.has_headers)
.delimiter(self.delimiter)
.from_path(&self.path)
.map_err(|error| Error::ReadFailed {
path: self.path.clone(),
reason: error.to_string(),
})?;
let headers: Vec<String> = if self.has_headers {
reader
.headers()
.map_err(|error| Error::ReadFailed {
path: self.path.clone(),
reason: format!("failed to read CSV headers: {error}"),
})?
.iter()
.map(String::from)
.collect()
} else {
Vec::new()
};
Ok(Box::new(CsvIterator {
reader,
headers,
path: self.path.clone(),
index: 0,
}))
}
fn name(&self) -> &str {
self.path.to_str().map_or("csv", |s| s)
}
}
struct CsvIterator {
reader: csv::Reader<std::fs::File>,
headers: Vec<String>,
path: PathBuf,
index: u64,
}
impl SourceIterator for CsvIterator {
fn next_sample(&mut self) -> Option<Result<Sample>> {
let mut record = csv::StringRecord::new();
match self.reader.read_record(&mut record) {
Ok(true) => {}
Ok(false) => return None, Err(error) => {
return Some(Err(Error::CorruptData {
path: self.path.clone(),
reason: format!("CSV parse error on row {}: {error}", self.index + 1),
}));
}
}
let index = self.index;
self.index += 1;
let mut sample = Sample::new();
while self.headers.len() < record.len() {
self.headers.push(format!("col_{}", self.headers.len()));
}
for (col_idx, value) in record.iter().enumerate() {
let field_name = self.headers[col_idx].as_str();
let tensor = parse_csv_value(value);
sample = sample.with(field_name, tensor);
}
Some(Ok(sample.with_metadata(self.path.to_string_lossy(), index)))
}
}
fn parse_csv_value(value: &str) -> Tensor {
if let Ok(number) = value.parse::<f64>() {
Tensor::f64(&[number], vec![1])
} else {
Tensor::bytes(value.as_bytes().to_vec())
}
}