#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Table {
pub header: Option<Vec<String>>,
pub rows: Vec<Vec<String>>,
}
impl Table {
pub fn width(&self) -> usize {
self.header
.iter()
.chain(&self.rows)
.map(Vec::len)
.max()
.unwrap_or(0)
}
}
pub fn frame(text: &str, delimiter: Option<char>, header: bool) -> Table {
let mut lines = text
.lines()
.map(|line| split(line, delimiter))
.filter(|fields| !fields.is_empty());
let header = if header { lines.next() } else { None };
Table {
header,
rows: lines.collect(),
}
}
fn split(line: &str, delimiter: Option<char>) -> Vec<String> {
match delimiter {
None => line.split_whitespace().map(str::to_owned).collect(),
Some(sep) => {
if line.trim().is_empty() {
Vec::new()
} else {
line.split(sep).map(str::to_owned).collect()
}
}
}
}
pub fn column_index(table: &Table, selector: &str) -> Result<usize, String> {
if !selector.is_empty() && selector.bytes().all(|byte| byte.is_ascii_digit()) {
let index = selector
.parse::<usize>()
.map_err(|_| format!("column index `{selector}` is too large"))?;
let width = table.width();
return if index < width {
Ok(index)
} else if width == 0 {
Err(format!(
"column index {index} is out of range: the input has no columns"
))
} else {
Err(format!(
"column index {index} is out of range: the input has {width} columns \
(valid indices: 0..={})",
width - 1
))
};
}
match &table.header {
Some(names) => names
.iter()
.position(|name| name == selector)
.ok_or_else(|| {
format!(
"no column named `{selector}`; the header has: {}",
names.join(", ")
)
}),
None => Err(format!(
"no column named `{selector}`: selecting by name needs a header row (-H); \
0-based indices always work"
)),
}
}
pub fn select(table: &Table, selectors: &[String]) -> Result<Table, String> {
let indices = selectors
.iter()
.map(|selector| column_index(table, selector))
.collect::<Result<Vec<_>, _>>()?;
let project = |row: &Vec<String>| -> Vec<String> {
indices
.iter()
.map(|&index| row.get(index).cloned().unwrap_or_default())
.collect()
};
Ok(Table {
header: table.header.as_ref().map(project),
rows: table.rows.iter().map(project).collect(),
})
}
pub fn string_column(table: &Table, index: usize) -> Vec<String> {
table
.rows
.iter()
.map(|row| {
let field = row.get(index).map(String::as_str).unwrap_or("");
if field.is_empty() { "-" } else { field }.to_owned()
})
.collect()
}
#[cfg(test)]
#[path = "tests/input_tests.rs"]
mod tests;