109 lines
4.1 KiB
Rust
109 lines
4.1 KiB
Rust
use crate::csv::row::CsvRow;
|
|
use crate::csv::table::{CsvColumn, CsvTable};
|
|
use crate::csv::value::{ColumnType, CsvValue};
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum CsvError {
|
|
#[error("{file}: the file has no column name line")]
|
|
MissingColumnNames { file: String },
|
|
#[error("{file}: the file has no column type line")]
|
|
MissingColumnTypes { file: String },
|
|
#[error("{file}: column {column} (`{name}`) has the unknown type `{value}`, expecting int/string/boolean")]
|
|
UnknownColumnType {
|
|
file: String,
|
|
column: usize,
|
|
name: String,
|
|
value: String,
|
|
},
|
|
#[error("{file}: the first data line does not open a row")]
|
|
OrphanValues { file: String },
|
|
#[error("io: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
pub struct CsvReader;
|
|
impl CsvReader {
|
|
pub fn parse(file_name: impl Into<String>, source: &str) -> Result<CsvTable, CsvError> {
|
|
let file_name = file_name.into();
|
|
let source = source.strip_prefix('\u{feff}').unwrap_or(source);
|
|
let mut lines = source
|
|
.lines()
|
|
.map(|line| line.strip_suffix('\r').unwrap_or(line));
|
|
let name_line = lines.next().ok_or_else(|| CsvError::MissingColumnNames {
|
|
file: file_name.clone(),
|
|
})?;
|
|
let type_line = lines.next().ok_or_else(|| CsvError::MissingColumnTypes {
|
|
file: file_name.clone(),
|
|
})?;
|
|
let names = split_line(name_line);
|
|
let types = split_line(type_line);
|
|
let mut columns = Vec::with_capacity(names.len());
|
|
for (index, name) in names.iter().enumerate() {
|
|
let raw = types
|
|
.get(index)
|
|
.map(|value| value.trim())
|
|
.filter(|value| !value.is_empty())
|
|
.unwrap_or("string");
|
|
let column_type =
|
|
ColumnType::parse(raw).ok_or_else(|| CsvError::UnknownColumnType {
|
|
file: file_name.clone(),
|
|
column: index,
|
|
name: name.clone(),
|
|
value: raw.to_owned(),
|
|
})?;
|
|
columns.push(CsvColumn {
|
|
name: name.clone(),
|
|
column_type,
|
|
});
|
|
}
|
|
let mut rows: Vec<CsvRow> = Vec::new();
|
|
for line in lines {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
let cells = split_line(line);
|
|
let opens_row = cells
|
|
.first()
|
|
.map(|cell| !cell.trim().is_empty())
|
|
.unwrap_or(false);
|
|
if opens_row {
|
|
rows.push(CsvRow::with_columns(columns.len()));
|
|
} else if rows.is_empty() {
|
|
return Err(CsvError::OrphanValues { file: file_name });
|
|
}
|
|
let row = rows.last_mut().expect("row present");
|
|
for (index, column) in columns.iter().enumerate() {
|
|
let raw = cells.get(index).map(String::as_str).unwrap_or("");
|
|
row.push(index, CsvValue::convert(raw, column.column_type));
|
|
}
|
|
}
|
|
Ok(CsvTable::new(file_name, columns, rows))
|
|
}
|
|
pub fn parse_file(path: impl AsRef<std::path::Path>) -> Result<CsvTable, CsvError> {
|
|
let path = path.as_ref();
|
|
let bytes = std::fs::read(path)?;
|
|
let source = String::from_utf8_lossy(&bytes).into_owned();
|
|
let file_name = path
|
|
.file_name()
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
.unwrap_or_default();
|
|
Self::parse(file_name, &source)
|
|
}
|
|
}
|
|
fn split_line(line: &str) -> Vec<String> {
|
|
let mut cells = Vec::new();
|
|
let mut current = String::new();
|
|
let mut quoted = false;
|
|
let mut chars = line.chars().peekable();
|
|
while let Some(character) = chars.next() {
|
|
match character {
|
|
'"' if quoted && chars.peek() == Some(&'"') => {
|
|
current.push('"');
|
|
chars.next();
|
|
}
|
|
'"' => quoted = !quoted,
|
|
',' if !quoted => cells.push(std::mem::take(&mut current)),
|
|
_ => current.push(character),
|
|
}
|
|
}
|
|
cells.push(current);
|
|
cells
|
|
}
|