#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Unread {
Unterminated { line: usize },
TooManyRecords,
TooManyCells { record: usize },
}
pub(super) type Record = (usize, Vec<String>);
pub(super) fn records(
text: &str,
max_records: usize,
max_cells: usize,
) -> Result<Vec<Record>, Unread> {
let text = text.strip_prefix('\u{FEFF}').unwrap_or(text);
let delimiter = delimiter(text);
let mut reader = Reader {
records: Vec::new(),
record: Vec::new(),
number: 0,
max_records,
max_cells,
};
let mut cell = String::new();
let mut fresh = true;
let mut quoted = None;
let mut line = 1;
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if quoted.is_some() {
match c {
'"' if chars.peek() == Some(&'"') => {
chars.next();
cell.push('"');
}
'"' => quoted = None,
_ => {
if c == '\n' || (c == '\r' && chars.peek() != Some(&'\n')) {
line += 1;
}
cell.push(c);
}
}
continue;
}
match c {
'"' if fresh => {
quoted = Some(line);
fresh = false;
}
'\r' if chars.peek() == Some(&'\n') => {}
'\n' | '\r' => {
reader.cell(std::mem::take(&mut cell))?;
reader.end()?;
fresh = true;
line += 1;
}
c if c == delimiter => {
reader.cell(std::mem::take(&mut cell))?;
fresh = true;
}
c => {
cell.push(c);
fresh = false;
}
}
}
if let Some(line) = quoted {
return Err(Unread::Unterminated { line });
}
if !fresh || !reader.record.is_empty() {
reader.cell(cell)?;
reader.end()?;
}
Ok(reader.records)
}
struct Reader {
records: Vec<Record>,
record: Vec<String>,
number: usize,
max_records: usize,
max_cells: usize,
}
impl Reader {
fn cell(&mut self, cell: String) -> Result<(), Unread> {
if self.record.len() == self.max_cells {
return Err(Unread::TooManyCells {
record: self.number + 1,
});
}
self.record.push(cell);
Ok(())
}
fn end(&mut self) -> Result<(), Unread> {
self.number += 1;
if self.record.iter().all(|cell| cell.trim().is_empty()) {
self.record.clear();
return Ok(());
}
if self.records.len() == self.max_records {
return Err(Unread::TooManyRecords);
}
self.records
.push((self.number, std::mem::take(&mut self.record)));
Ok(())
}
}
fn delimiter(text: &str) -> char {
let mut quoted = false;
let mut fresh = true;
let mut semicolon = false;
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if quoted {
if c == '"' && chars.next_if_eq(&'"').is_none() {
quoted = false;
}
continue;
}
match c {
'"' if fresh => {
quoted = true;
fresh = false;
}
',' => return ',',
';' => {
semicolon = true;
fresh = true;
}
'\n' | '\r' => break,
_ => fresh = false,
}
}
if semicolon { ';' } else { ',' }
}
#[cfg(test)]
mod tests;