scroll.server/crates/titan/src/csv/reader.rs
WiseDev d25a6de423 move all crates into crates/, glob members
pure move, no code touched. readme and protocol.md paths fixed up.
2026-08-23 08:15:45 +03:00

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
}