use quarb::number::Decimal;
use quarb::{AstAdapter, NodeId, Value};
#[derive(Debug, thiserror::Error)]
pub enum CsvError {
#[error(transparent)]
Csv(#[from] csv::Error),
#[error(
"the header has no {decided:?} but {found:?} throughout; say the delimiter with \
?delimiter={name}"
)]
Delimiter {
decided: char,
found: char,
name: &'static str,
},
#[error("unknown delimiter {0:?} (a character, or comma, semicolon, tab, pipe)")]
DelimiterLabel(String),
#[error("unknown decimal convention {0:?} (point or comma)")]
DecimalLabel(String),
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CsvOptions {
pub delimiter: Option<u8>,
pub decimal: Decimal,
}
impl CsvOptions {
pub fn with_delimiter_label(mut self, label: &str) -> Result<Self, CsvError> {
self.delimiter = Some(delimiter_label(label)?);
Ok(self)
}
pub fn with_decimal_label(mut self, label: &str) -> Result<Self, CsvError> {
self.decimal =
Decimal::from_label(label).ok_or_else(|| CsvError::DecimalLabel(label.into()))?;
Ok(self)
}
}
pub fn delimiter_label(label: &str) -> Result<u8, CsvError> {
match label {
"comma" => Ok(b','),
"semicolon" => Ok(b';'),
"tab" => Ok(b'\t'),
"pipe" => Ok(b'|'),
_ => {
let mut chars = label.chars();
match (chars.next(), chars.next()) {
(Some(c), None) if c.is_ascii() && c != '"' && c != '\n' => Ok(c as u8),
_ => Err(CsvError::DelimiterLabel(label.into())),
}
}
}
}
fn delimiter_name(d: u8) -> &'static str {
match d {
b',' => "comma",
b';' => "semicolon",
b'\t' => "tab",
b'|' => "pipe",
_ => "CHAR",
}
}
pub struct CsvAdapter {
columns: Vec<String>,
rows: Vec<Vec<String>>,
delimiter: u8,
decimal: Decimal,
}
impl CsvAdapter {
pub fn parse(text: &str) -> Result<Self, csv::Error> {
Self::parse_with_delimiter(text, b',')
}
pub fn parse_with_delimiter(text: &str, delimiter: u8) -> Result<Self, csv::Error> {
match Self::parse_declared(text, delimiter, CsvOptions::default()) {
Ok(a) => Ok(a),
Err(CsvError::Csv(e)) => Err(e),
Err(_) => Self::read(text, delimiter, Decimal::Point),
}
}
pub fn parse_declared(
text: &str,
by_extension: u8,
options: CsvOptions,
) -> Result<Self, CsvError> {
let (text, declared) = match sep_line(text) {
Some((d, rest)) => (rest, Some(d)),
None => (text, None),
};
let delimiter = options.delimiter.or(declared).unwrap_or(by_extension);
let header = text.lines().next().unwrap_or("");
if !header.as_bytes().contains(&delimiter) {
for candidate in [b',', b';', b'\t', b'|'] {
if candidate != delimiter
&& header.as_bytes().contains(&candidate)
&& text
.lines()
.filter(|l| !l.trim().is_empty())
.all(|l| l.as_bytes().contains(&candidate))
{
return Err(CsvError::Delimiter {
decided: delimiter as char,
found: candidate as char,
name: delimiter_name(candidate),
});
}
}
}
Ok(Self::read(text, delimiter, options.decimal)?)
}
fn read(text: &str, delimiter: u8, decimal: Decimal) -> Result<Self, csv::Error> {
let mut reader = csv::ReaderBuilder::new()
.delimiter(delimiter)
.from_reader(text.as_bytes());
let columns: Vec<String> = reader
.headers()?
.iter()
.map(|h| h.trim().to_string())
.collect();
let mut rows = Vec::new();
for record in reader.records() {
let record = record?;
rows.push(record.iter().map(|c| c.to_string()).collect());
}
Ok(CsvAdapter {
columns,
rows,
delimiter,
decimal,
})
}
pub fn locator(&self, node: NodeId) -> String {
if node.0 == 0 {
"/".to_string()
} else {
format!("/row[{}]", node.0)
}
}
fn canonical(&self, cell: &str) -> String {
if self.decimal == Decimal::Comma
&& let Some(c) = quarb::number::canonical(cell.trim(), Decimal::Comma)
{
return c;
}
cell.to_string()
}
fn cell(&self, node: NodeId, column: &str) -> Option<&str> {
let row = self.rows.get((node.0 as usize).checked_sub(1)?)?;
let idx = self.columns.iter().position(|c| c == column)?;
row.get(idx).map(|c| c.as_str()).filter(|c| !c.is_empty())
}
}
impl AstAdapter for CsvAdapter {
fn estimate(&self, scope: &[&str], plan: &quarb::StepPlan<'_>) -> Option<quarb::Estimate> {
quarb::adapter::arena_estimate(self, scope, plan, quarb::Latency::Memory, None)
}
fn resolves_references(&self) -> bool {
false
}
fn root(&self) -> NodeId {
NodeId(0)
}
fn walks_for_references(&self) -> bool {
true
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
if node.0 == 0 {
(1..=self.rows.len() as u64).map(NodeId).collect()
} else {
Vec::new()
}
}
fn name(&self, node: NodeId) -> Option<String> {
if node.0 == 0 {
None
} else {
Some("row".to_string())
}
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
if node.0 == 0 { None } else { Some(NodeId(0)) }
}
fn property(&self, node: NodeId, name: &str) -> Option<Value> {
self.cell(node, name).map(|c| Value::Str(self.canonical(c)))
}
fn properties(&self, node: NodeId) -> Vec<(String, Value)> {
if node.0 == 0 {
return Vec::new();
}
self.columns
.iter()
.filter_map(|c| {
self.cell(node, c)
.map(|v| (c.clone(), Value::Str(self.canonical(v))))
})
.collect()
}
fn metadata_catalogue(&self) -> &'static [&'static str] {
&["delimiter", "decimal", "columns", "n-rows", "n-fields"]
}
fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
if node.0 == 0 {
match key {
"delimiter" => return Some(Value::Str((self.delimiter as char).to_string())),
"decimal" => {
return Some(Value::Str(
match self.decimal {
Decimal::Point => "point",
Decimal::Comma => "comma",
}
.into(),
));
}
_ => {}
}
return match key {
"columns" => Some(Value::list(
self.columns.iter().map(|c| Value::Str(c.clone())).collect(),
)),
"n-rows" => Some(Value::Int(self.rows.len() as i64)),
_ => None,
};
}
match key {
"n-fields" => self
.rows
.get(node.0 as usize - 1)
.map(|r| Value::Int(r.len() as i64)),
other => self.property(node, other),
}
}
}
fn sep_line(text: &str) -> Option<(u8, &str)> {
let rest = text.strip_prefix("sep=")?;
let mut chars = rest.chars();
let d = chars.next()?;
let after = chars.as_str();
let after = after
.strip_prefix("\r\n")
.or_else(|| after.strip_prefix('\n'))?;
(d.is_ascii() && d != '\n').then_some((d as u8, after))
}