1use quarb::number::Decimal;
34use quarb::{AstAdapter, NodeId, Value};
35
36#[derive(Debug, thiserror::Error)]
37pub enum CsvError {
38 #[error(transparent)]
39 Csv(#[from] csv::Error),
40 #[error(
41 "the header has no {decided:?} but {found:?} throughout; say the delimiter with \
42 ?delimiter={name}"
43 )]
44 Delimiter {
45 decided: char,
46 found: char,
47 name: &'static str,
48 },
49 #[error("unknown delimiter {0:?} (a character, or comma, semicolon, tab, pipe)")]
50 DelimiterLabel(String),
51 #[error("unknown decimal convention {0:?} (point or comma)")]
52 DecimalLabel(String),
53}
54
55#[derive(Clone, Copy, Debug, Default)]
58pub struct CsvOptions {
59 pub delimiter: Option<u8>,
61 pub decimal: Decimal,
63}
64
65impl CsvOptions {
66 pub fn with_delimiter_label(mut self, label: &str) -> Result<Self, CsvError> {
70 self.delimiter = Some(delimiter_label(label)?);
71 Ok(self)
72 }
73
74 pub fn with_decimal_label(mut self, label: &str) -> Result<Self, CsvError> {
75 self.decimal =
76 Decimal::from_label(label).ok_or_else(|| CsvError::DecimalLabel(label.into()))?;
77 Ok(self)
78 }
79}
80
81pub fn delimiter_label(label: &str) -> Result<u8, CsvError> {
83 match label {
84 "comma" => Ok(b','),
85 "semicolon" => Ok(b';'),
86 "tab" => Ok(b'\t'),
87 "pipe" => Ok(b'|'),
88 _ => {
89 let mut chars = label.chars();
90 match (chars.next(), chars.next()) {
91 (Some(c), None) if c.is_ascii() && c != '"' && c != '\n' => Ok(c as u8),
92 _ => Err(CsvError::DelimiterLabel(label.into())),
93 }
94 }
95 }
96}
97
98fn delimiter_name(d: u8) -> &'static str {
99 match d {
100 b',' => "comma",
101 b';' => "semicolon",
102 b'\t' => "tab",
103 b'|' => "pipe",
104 _ => "CHAR",
105 }
106}
107
108pub struct CsvAdapter {
110 columns: Vec<String>,
112 rows: Vec<Vec<String>>,
114 delimiter: u8,
116 decimal: Decimal,
118}
119
120impl CsvAdapter {
121 pub fn parse(text: &str) -> Result<Self, csv::Error> {
124 Self::parse_with_delimiter(text, b',')
125 }
126
127 pub fn parse_with_delimiter(text: &str, delimiter: u8) -> Result<Self, csv::Error> {
129 match Self::parse_declared(text, delimiter, CsvOptions::default()) {
130 Ok(a) => Ok(a),
131 Err(CsvError::Csv(e)) => Err(e),
132 Err(_) => Self::read(text, delimiter, Decimal::Point),
136 }
137 }
138
139 pub fn parse_declared(
145 text: &str,
146 by_extension: u8,
147 options: CsvOptions,
148 ) -> Result<Self, CsvError> {
149 let (text, declared) = match sep_line(text) {
150 Some((d, rest)) => (rest, Some(d)),
151 None => (text, None),
152 };
153 let delimiter = options.delimiter.or(declared).unwrap_or(by_extension);
154 let header = text.lines().next().unwrap_or("");
155 if !header.as_bytes().contains(&delimiter) {
156 for candidate in [b',', b';', b'\t', b'|'] {
157 if candidate != delimiter
158 && header.as_bytes().contains(&candidate)
159 && text
160 .lines()
161 .filter(|l| !l.trim().is_empty())
162 .all(|l| l.as_bytes().contains(&candidate))
163 {
164 return Err(CsvError::Delimiter {
165 decided: delimiter as char,
166 found: candidate as char,
167 name: delimiter_name(candidate),
168 });
169 }
170 }
171 }
172 Ok(Self::read(text, delimiter, options.decimal)?)
173 }
174
175 fn read(text: &str, delimiter: u8, decimal: Decimal) -> Result<Self, csv::Error> {
176 let mut reader = csv::ReaderBuilder::new()
177 .delimiter(delimiter)
178 .from_reader(text.as_bytes());
179 let columns: Vec<String> = reader
180 .headers()?
181 .iter()
182 .map(|h| h.trim().to_string())
183 .collect();
184 let mut rows = Vec::new();
185 for record in reader.records() {
186 let record = record?;
187 rows.push(record.iter().map(|c| c.to_string()).collect());
188 }
189 Ok(CsvAdapter {
190 columns,
191 rows,
192 delimiter,
193 decimal,
194 })
195 }
196
197 pub fn locator(&self, node: NodeId) -> String {
199 if node.0 == 0 {
200 "/".to_string()
201 } else {
202 format!("/row[{}]", node.0)
203 }
204 }
205
206 fn canonical(&self, cell: &str) -> String {
210 if self.decimal == Decimal::Comma
211 && let Some(c) = quarb::number::canonical(cell.trim(), Decimal::Comma)
212 {
213 return c;
214 }
215 cell.to_string()
216 }
217
218 fn cell(&self, node: NodeId, column: &str) -> Option<&str> {
219 let row = self.rows.get((node.0 as usize).checked_sub(1)?)?;
222 let idx = self.columns.iter().position(|c| c == column)?;
223 row.get(idx).map(|c| c.as_str()).filter(|c| !c.is_empty())
225 }
226}
227
228impl AstAdapter for CsvAdapter {
229 fn estimate(&self, scope: &[&str], plan: &quarb::StepPlan<'_>) -> Option<quarb::Estimate> {
231 quarb::adapter::arena_estimate(self, scope, plan, quarb::Latency::Memory, None)
232 }
233
234 fn resolves_references(&self) -> bool {
236 false
237 }
238
239 fn root(&self) -> NodeId {
240 NodeId(0)
241 }
242
243 fn walks_for_references(&self) -> bool {
246 true
247 }
248
249 fn children(&self, node: NodeId) -> Vec<NodeId> {
250 if node.0 == 0 {
251 (1..=self.rows.len() as u64).map(NodeId).collect()
252 } else {
253 Vec::new()
254 }
255 }
256
257 fn name(&self, node: NodeId) -> Option<String> {
258 if node.0 == 0 {
259 None
260 } else {
261 Some("row".to_string())
262 }
263 }
264
265 fn parent(&self, node: NodeId) -> Option<NodeId> {
266 if node.0 == 0 { None } else { Some(NodeId(0)) }
267 }
268
269 fn property(&self, node: NodeId, name: &str) -> Option<Value> {
275 self.cell(node, name).map(|c| Value::Str(self.canonical(c)))
276 }
277
278 fn properties(&self, node: NodeId) -> Vec<(String, Value)> {
282 if node.0 == 0 {
283 return Vec::new();
284 }
285 self.columns
286 .iter()
287 .filter_map(|c| {
288 self.cell(node, c)
289 .map(|v| (c.clone(), Value::Str(self.canonical(v))))
290 })
291 .collect()
292 }
293
294 fn metadata_catalogue(&self) -> &'static [&'static str] {
300 &["delimiter", "decimal", "columns", "n-rows", "n-fields"]
301 }
302
303 fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
304 if node.0 == 0 {
305 match key {
306 "delimiter" => return Some(Value::Str((self.delimiter as char).to_string())),
307 "decimal" => {
308 return Some(Value::Str(
309 match self.decimal {
310 Decimal::Point => "point",
311 Decimal::Comma => "comma",
312 }
313 .into(),
314 ));
315 }
316 _ => {}
317 }
318 return match key {
319 "columns" => Some(Value::list(
320 self.columns.iter().map(|c| Value::Str(c.clone())).collect(),
321 )),
322 "n-rows" => Some(Value::Int(self.rows.len() as i64)),
323 _ => None,
324 };
325 }
326 match key {
327 "n-fields" => self
328 .rows
329 .get(node.0 as usize - 1)
330 .map(|r| Value::Int(r.len() as i64)),
331 other => self.property(node, other),
332 }
333 }
334}
335
336fn sep_line(text: &str) -> Option<(u8, &str)> {
339 let rest = text.strip_prefix("sep=")?;
340 let mut chars = rest.chars();
341 let d = chars.next()?;
342 let after = chars.as_str();
343 let after = after
344 .strip_prefix("\r\n")
345 .or_else(|| after.strip_prefix('\n'))?;
346 (d.is_ascii() && d != '\n').then_some((d as u8, after))
347}