#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strictness {
Lenient,
Strict,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub line: usize,
pub fields: Vec<String>,
}
impl Record {
pub fn is_blank(&self) -> bool {
self.fields.iter().all(|field| field.trim().is_empty())
}
}
pub fn strip_bom(text: &str) -> &str {
text.strip_prefix('\u{feff}').unwrap_or(text)
}
pub fn sniff_separator(text: &str) -> char {
let first_line = text.lines().find(|line| !line.trim().is_empty()).unwrap_or("");
if first_line.contains('\t') && !first_line.contains(',') {
'\t'
} else {
','
}
}
pub fn parse(text: &str, separator: char, strictness: Strictness) -> Result<Vec<Record>, String> {
let strict = strictness == Strictness::Strict;
let mut records = Vec::new();
let mut fields = Vec::new();
let mut field = String::new();
let mut quoted = false;
let mut closed = false;
let mut line = 1;
let mut record_line = 1;
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if quoted {
match c {
'"' if chars.peek() == Some(&'"') => {
chars.next();
field.push('"');
}
'"' => {
quoted = false;
closed = true;
}
'\n' => {
line += 1;
field.push('\n');
}
_ => field.push(c),
}
continue;
}
match c {
'"' if !closed && field.trim().is_empty() => {
field.clear();
quoted = true;
}
c if c == separator => {
fields.push(std::mem::take(&mut field));
closed = false;
}
'\r' | '\n' => {
if c == '\r' && chars.peek() == Some(&'\n') {
chars.next();
}
fields.push(std::mem::take(&mut field));
records.push(Record { line: record_line, fields: std::mem::take(&mut fields) });
closed = false;
line += 1;
record_line = line;
}
c if closed && strict => {
if !c.is_whitespace() {
return Err(format!("line {line}: text after a closing quote"));
}
}
_ => field.push(c),
}
}
if quoted && strict {
return Err(format!("line {record_line}: a quoted field is never closed"));
}
if !field.is_empty() || !fields.is_empty() || closed || quoted {
fields.push(field);
records.push(Record { line: record_line, fields });
}
Ok(records)
}