use std::path::Path;
use anyhow::{Context, Result};
pub const MAX_ROWS: usize = 100_000;
#[derive(Debug, Clone, Default)]
pub struct TableData {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub ncols: usize,
pub truncated: bool,
}
impl TableData {
pub fn header(&self, col: usize) -> &str {
self.headers.get(col).map(String::as_str).unwrap_or("")
}
pub fn cell(&self, row: usize, col: usize) -> &str {
self.rows
.get(row)
.and_then(|r| r.get(col))
.map(String::as_str)
.unwrap_or("")
}
pub fn nrows(&self) -> usize {
self.rows.len()
}
}
pub fn parse(path: &Path, delimiter: u8) -> Result<TableData> {
let mut rdr = csv::ReaderBuilder::new()
.delimiter(delimiter)
.flexible(true) .has_headers(false) .from_path(path)
.with_context(|| format!("open csv/tsv: {}", path.display()))?;
let decode = |rec: &csv::ByteRecord| -> Vec<String> {
rec.iter()
.map(|f| String::from_utf8_lossy(f).into_owned())
.collect()
};
let mut rec = csv::ByteRecord::new();
if !rdr
.read_byte_record(&mut rec)
.context("read csv/tsv header")?
{
return Ok(TableData::default());
}
let headers = decode(&rec);
let mut ncols = headers.len();
let mut rows: Vec<Vec<String>> = Vec::new();
let mut truncated = false;
while rdr.read_byte_record(&mut rec).context("read csv/tsv row")? {
if rows.len() >= MAX_ROWS {
truncated = true;
break;
}
let row = decode(&rec);
ncols = ncols.max(row.len());
rows.push(row);
}
Ok(TableData {
headers,
rows,
ncols,
truncated,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_temp(name: &str, content: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join("konoma_table_tests");
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join(name);
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(content.as_bytes()).unwrap();
p
}
#[test]
fn parses_headers_and_rows() {
let p = write_temp("basic.csv", "a,b,c\n1,2,3\n4,5,6\n");
let t = parse(&p, b',').unwrap();
assert_eq!(t.headers, vec!["a", "b", "c"]);
assert_eq!(t.nrows(), 2);
assert_eq!(t.ncols, 3);
assert_eq!(t.cell(0, 0), "1");
assert_eq!(t.cell(1, 2), "6");
assert_eq!(t.header(1), "b");
assert!(!t.truncated);
}
#[test]
fn quoted_comma_stays_one_cell() {
let p = write_temp("quoted.csv", "name,note\n\"Doe, John\",hi\n");
let t = parse(&p, b',').unwrap();
assert_eq!(t.cell(0, 0), "Doe, John");
assert_eq!(t.cell(0, 1), "hi");
}
#[test]
fn tab_delimiter_for_tsv() {
let p = write_temp("basic.tsv", "x\ty\n10\t20\n");
let t = parse(&p, b'\t').unwrap();
assert_eq!(t.headers, vec!["x", "y"]);
assert_eq!(t.cell(0, 1), "20");
}
#[test]
fn ragged_rows_report_max_columns() {
let p = write_temp("ragged.csv", "a,b,c\n1\n4,5,6,7\n");
let t = parse(&p, b',').unwrap();
assert_eq!(t.ncols, 4);
assert_eq!(t.cell(0, 2), ""); assert_eq!(t.cell(1, 3), "7");
}
#[test]
fn empty_file_is_empty_table() {
let p = write_temp("empty.csv", "");
let t = parse(&p, b',').unwrap();
assert!(t.headers.is_empty());
assert_eq!(t.nrows(), 0);
assert_eq!(t.ncols, 0);
}
}