main
John Lauer Publish 0.1.0 55a9481 2d ago
//! A small RFC 4180 CSV reader and writer (quoted fields, doubled quotes, commas and newlines in quotes).

pub fn parse(text: &str) -> Vec<Vec<String>> {
    let mut rows = Vec::new();
    let mut row = Vec::new();
    let mut field = String::new();
    let mut quoted = false;
    let mut chars = text.chars().peekable();
    let mut any = false;
    while let Some(c) = chars.next() {
        any = true;
        if quoted {
            if c == '"' {
                if chars.peek() == Some(&'"') { field.push('"'); chars.next(); } else { quoted = false; }
            } else { field.push(c); }
            continue;
        }
        match c {
            '"' => quoted = true,
            ',' => row.push(std::mem::take(&mut field)),
            '\r' => {}
            '\n' => { row.push(std::mem::take(&mut field)); rows.push(std::mem::take(&mut row)); any = false; }
            _ => field.push(c),
        }
    }
    if any { row.push(field); rows.push(row); }
    rows
}

/// Header and rows as maps, like Python's csv.DictReader (missing cells read as "").
pub struct Table { pub head: Vec<String>, pub rows: Vec<Vec<String>> }

impl Table {
    pub fn read(path: &std::path::Path) -> std::io::Result<Table> {
        let t = std::fs::read_to_string(path)?;
        let t = t.strip_prefix('\u{feff}').unwrap_or(&t);
        let mut all = parse(t);
        if all.is_empty() { return Ok(Table { head: vec![], rows: vec![] }); }
        let head = all.remove(0).into_iter().map(|h| h.trim().to_string()).collect();
        Ok(Table { head, rows: all.into_iter().filter(|r| !(r.len() == 1 && r[0].is_empty())).collect() })
    }
    pub fn col(&self, name: &str) -> Option<usize> { self.head.iter().position(|h| h == name) }
    pub fn get<'a>(&self, row: &'a [String], name: &str) -> &'a str {
        self.col(name).and_then(|i| row.get(i)).map(|s| s.as_str()).unwrap_or("")
    }
}

fn quote(f: &str) -> String {
    if f.contains([',', '"', '\n', '\r']) { format!("\"{}\"", f.replace('"', "\"\"")) } else { f.to_string() }
}

/// Rows joined with `eol` ("\r\n" matches Python's csv module default, which JLCPCB's files were made with).
pub fn write(rows: &[Vec<String>], eol: &str) -> String {
    rows.iter().map(|r| r.iter().map(|f| quote(f)).collect::<Vec<_>>().join(",") + eol).collect()
}

#[cfg(test)]
mod tests {
    #[test]
    fn quoted_fields_round_trip() {
        let rows = super::parse("a,\"b, c\",\"d \"\"e\"\"\"\r\n1,2,3\n");
        assert_eq!(rows, vec![vec!["a", "b, c", "d \"e\""], vec!["1", "2", "3"]]);
        assert_eq!(super::write(&rows, "\n"), "a,\"b, c\",\"d \"\"e\"\"\"\n1,2,3\n");
    }
}