use ndarray::Array1;
use super::Block;
use super::column::Column;
use crate::types::{F, I};
pub fn block_from_csv(
text: &str,
delimiter: char,
header: Option<&[String]>,
) -> Result<Block, String> {
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
let headers: Vec<String> = match header {
Some(h) => h.to_vec(),
None => lines
.next()
.ok_or_else(|| "CSV is empty".to_string())?
.split(delimiter)
.map(|s| s.trim().to_string())
.collect(),
};
let ncol = headers.len();
let mut cols: Vec<Vec<String>> = vec![Vec::new(); ncol];
for line in lines {
let fields: Vec<&str> = line.split(delimiter).collect();
if fields.len() < ncol {
return Err(format!(
"CSV row has {} field(s) but {} column name(s) were given",
fields.len(),
ncol
));
}
for (i, col) in cols.iter_mut().enumerate() {
col.push(fields[i].trim().to_string());
}
}
let mut block = Block::new();
for (name, raw) in headers.into_iter().zip(cols) {
insert_inferred(&mut block, name, raw)?;
}
Ok(block)
}
fn insert_inferred(block: &mut Block, name: String, raw: Vec<String>) -> Result<(), String> {
let nonempty = !raw.is_empty();
if nonempty && raw.iter().all(|s| s.parse::<I>().is_ok()) {
let v: Vec<I> = raw.iter().map(|s| s.parse().unwrap()).collect();
block
.insert(name, Array1::from(v).into_dyn())
.map_err(|e| e.to_string())
} else if nonempty && raw.iter().all(|s| s.parse::<F>().is_ok()) {
let v: Vec<F> = raw.iter().map(|s| s.parse().unwrap()).collect();
block
.insert(name, Array1::from(v).into_dyn())
.map_err(|e| e.to_string())
} else {
block
.insert(name, Array1::from(raw).into_dyn())
.map_err(|e| e.to_string())
}
}
pub fn block_to_csv(block: &Block, delimiter: char, header: bool) -> String {
let names: Vec<&str> = block.keys().collect();
let nrows = block.nrows().unwrap_or(0);
let delim = delimiter.to_string();
let mut out = String::new();
if header {
out.push_str(&names.join(&delim));
out.push('\n');
}
for row in 0..nrows {
let cells: Vec<String> = names
.iter()
.map(|name| match block.get(name) {
Some(col) => cell_to_string(col, row),
None => String::new(),
})
.collect();
out.push_str(&cells.join(&delim));
out.push('\n');
}
out
}
fn cell_to_string(col: &Column, row: usize) -> String {
if let Some(a) = col.as_float() {
return a[[row]].to_string();
}
if let Some(a) = col.as_int() {
return a[[row]].to_string();
}
if let Some(a) = col.as_uint() {
return a[[row]].to_string();
}
if let Some(a) = col.as_bool() {
return a[[row]].to_string();
}
if let Some(a) = col.as_u8() {
return a[[row]].to_string();
}
if let Some(a) = col.as_string() {
return a[[row]].clone();
}
String::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_headered() {
let text = "x,y,name\n0,1.5,a\n3,4.5,b\n";
let block = block_from_csv(text, ',', None).expect("parse");
assert_eq!(block.nrows(), Some(2));
assert!(block.get("x").unwrap().as_int().is_some());
assert!(block.get("y").unwrap().as_float().is_some());
assert!(block.get("name").unwrap().as_string().is_some());
let out = block_to_csv(&block, ',', true);
let rt = block_from_csv(&out, ',', None).expect("reparse");
assert_eq!(rt.get("x").unwrap().as_int().unwrap()[[1]], 3);
assert_eq!(rt.get("name").unwrap().as_string().unwrap()[[0]], "a");
}
#[test]
fn headerless_with_names() {
let names = vec!["a".to_string(), "b".to_string()];
let block = block_from_csv("1,2\n3,4\n", ',', Some(&names)).expect("parse");
assert_eq!(block.nrows(), Some(2));
let keys: std::collections::HashSet<&str> = block.keys().collect();
assert_eq!(keys, ["a", "b"].into_iter().collect());
}
#[test]
fn empty_text_errors() {
assert!(block_from_csv("", ',', None).is_err());
}
}