use csv::{ReaderBuilder, StringRecord};
use std::path::Path;
use crate::error::{PandRSError, Result};
use crate::series::Series;
use crate::DataFrame;
const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
fn read_csv_raw<P: AsRef<Path>>(
path: P,
has_header: bool,
) -> Result<(Vec<String>, Vec<Vec<String>>)> {
let mut bytes = std::fs::read(path.as_ref()).map_err(PandRSError::Io)?;
if bytes.starts_with(&UTF8_BOM) {
bytes.drain(0..UTF8_BOM.len());
}
let mut rdr = ReaderBuilder::new()
.has_headers(has_header)
.flexible(true)
.trim(csv::Trim::Headers)
.from_reader(bytes.as_slice());
let header_row: Option<Vec<String>> = if has_header {
Some(
rdr.headers()
.map_err(PandRSError::Csv)?
.iter()
.map(|h| h.to_string())
.collect(),
)
} else {
None
};
let all_records: Vec<StringRecord> = rdr
.records()
.collect::<std::result::Result<Vec<_>, csv::Error>>()
.map_err(PandRSError::Csv)?;
let headers: Vec<String> = match header_row {
Some(headers) => headers,
None => match all_records.first() {
Some(first) => (0..first.len()).map(|i| format!("column_{}", i)).collect(),
None => return Ok((Vec::new(), Vec::new())),
},
};
let mut columns: Vec<Vec<String>> = (0..headers.len())
.map(|_| Vec::with_capacity(all_records.len()))
.collect();
for (row_idx, record) in all_records.iter().enumerate() {
if record.len() != headers.len() {
return Err(PandRSError::Format(format!(
"CSV data row {} has {} field(s) but the header defines {} column(s); \
ragged rows are not supported",
row_idx + 1,
record.len(),
headers.len()
)));
}
for (col, field) in columns.iter_mut().zip(record.iter()) {
col.push(field.to_string());
}
}
Ok((headers, columns))
}
pub fn read_csv<P: AsRef<Path>>(path: P, has_header: bool) -> Result<DataFrame> {
let (headers, columns) = read_csv_raw(path, has_header)?;
let mut df = DataFrame::new();
for (header, values) in headers.into_iter().zip(columns) {
let series = Series::new(values, Some(header.clone()))?;
df.add_column(header, series)?;
}
Ok(df)
}
pub fn read_csv_typed<P: AsRef<Path>>(path: P, has_header: bool) -> Result<DataFrame> {
let (headers, columns) = read_csv_raw(path, has_header)?;
let mut df = DataFrame::new();
for (header, values) in headers.into_iter().zip(columns) {
add_inferred_column(&mut df, header, values)?;
}
Ok(df)
}
fn is_bool_token(s: &str) -> bool {
matches!(
s.to_lowercase().as_str(),
"true" | "false" | "1" | "0" | "yes" | "no" | "t" | "f"
)
}
fn parse_bool_token(s: &str) -> bool {
matches!(s.to_lowercase().as_str(), "true" | "1" | "yes" | "t")
}
fn add_inferred_column(df: &mut DataFrame, header: String, values: Vec<String>) -> Result<()> {
let has_missing = values.iter().any(|v| v.is_empty());
let non_empty: Vec<&str> = values
.iter()
.filter(|v| !v.is_empty())
.map(String::as_str)
.collect();
if non_empty.is_empty() {
let series = Series::new(values, Some(header.clone()))?;
return df.add_column(header, series);
}
if !has_missing && non_empty.iter().all(|s| s.parse::<i64>().is_ok()) {
let mut parsed = Vec::with_capacity(values.len());
for s in &values {
let v = s.parse::<i64>().map_err(|e| {
PandRSError::Format(format!(
"CSV column '{}': failed to parse '{}' as an integer: {}",
header, s, e
))
})?;
parsed.push(v);
}
let series = Series::new(parsed, Some(header.clone()))?;
return df.add_column(header, series);
}
if non_empty.iter().all(|s| s.parse::<f64>().is_ok()) {
let mut parsed = Vec::with_capacity(values.len());
for s in &values {
if s.is_empty() {
parsed.push(f64::NAN);
continue;
}
let v = s.parse::<f64>().map_err(|e| {
PandRSError::Format(format!(
"CSV column '{}': failed to parse '{}' as a float: {}",
header, s, e
))
})?;
parsed.push(v);
}
let series = Series::new(parsed, Some(header.clone()))?;
return df.add_column(header, series);
}
if !has_missing && non_empty.iter().all(|s| is_bool_token(s)) {
let parsed: Vec<bool> = values.iter().map(|s| parse_bool_token(s)).collect();
let series = Series::new(parsed, Some(header.clone()))?;
return df.add_column(header, series);
}
let series = Series::new(values, Some(header.clone()))?;
df.add_column(header, series)
}
pub fn write_csv<P: AsRef<Path>>(df: &DataFrame, path: P) -> Result<()> {
DataFrame::to_csv(df, path)
}