use std::error::Error;
use std::fmt::Write;
use std::process;
use std::result;
use clap::{crate_authors, crate_version, App, Arg};
use csv::{ReaderBuilder, StringRecord, Trim};
type Result<T> = result::Result<T, Box<dyn Error>>;
pub struct Table {
records: Vec<StringRecord>,
widths: Vec<usize>,
}
impl Table {
pub fn from_path(path: &str) -> Result<Self> {
let records = Self::parse_records(path)?;
let widths = Self::calculate_widths(&records);
Ok(Self { records, widths })
}
pub fn basic_format(&self, spaces: usize) -> Result<String> {
let mut output = String::new();
for record in &self.records {
for (i, field) in record.iter().enumerate() {
write!(output, "{:width$}", field, width = self.widths[i] + spaces)?;
}
let len = output.rfind(|c| !char::is_whitespace(c)).unwrap_or(0) + 1;
output.truncate(len);
writeln!(output)?;
}
Ok(output)
}
pub fn fancy_format(&self, headers: bool, separators: bool, spaces: usize) -> Result<String> {
let mut output = String::new();
for (i, width) in self.widths.iter().enumerate() {
let vertical = match i {
0 => "┌",
_ => "┬",
};
write!(
output,
"{}{:─<width$}",
vertical,
"",
width = width + spaces * 2
)?;
}
writeln!(output, "┐")?;
for (i, record) in self.records.iter().enumerate() {
if (separators && i > 0) || (headers && i == 1) {
for (j, width) in self.widths.iter().enumerate() {
let vertical = match j {
0 => "├",
_ => "┼",
};
write!(
output,
"{}{:─<width$}",
vertical,
"",
width = width + spaces * 2
)?;
}
writeln!(output, "┤")?;
}
for (j, field) in record.iter().enumerate() {
write!(
output,
"│{:<spaces$}{:width$}{:<spaces$}",
"",
field,
"",
spaces = spaces,
width = self.widths[j]
)?;
}
writeln!(output, "│")?;
}
for (i, width) in self.widths.iter().enumerate() {
let vertical = match i {
0 => "└",
_ => "┴",
};
write!(
output,
"{}{:─<width$}",
vertical,
"",
width = width + spaces * 2
)?;
}
writeln!(output, "┘")?;
Ok(output)
}
fn calculate_widths(records: &[StringRecord]) -> Vec<usize> {
let len = records.first().map_or(0, |r| r.len());
records.iter().fold(vec![0; len], |acc, r| {
acc.iter()
.zip(r.iter())
.map(|e| (*e.0).max(e.1.len()))
.collect()
})
}
fn parse_records(path: &str) -> csv::Result<Vec<StringRecord>> {
ReaderBuilder::new()
.has_headers(false)
.trim(Trim::All)
.from_path(path)?
.records()
.collect()
}
}
fn main() {
let matches = App::new("rtab")
.version(crate_version!())
.author(crate_authors!())
.about("Generate tables from CSV.")
.arg(Arg::with_name("file").required(true).value_name("FILE"))
.arg(
Arg::with_name("style")
.long("style")
.help("Sets table style")
.takes_value(true)
.possible_values(&["basic", "fancy"])
.default_value("basic")
.hide_default_value(true)
.value_name("STYLE"),
)
.arg(
Arg::with_name("headers")
.long("headers")
.help("Use separators for first row in fancy tables"),
)
.arg(
Arg::with_name("separators")
.long("separators")
.help("Use separators for all rows in fancy tables"),
)
.arg(
Arg::with_name("spaces")
.long("spaces")
.short("s")
.help("Number of spaces to use between fields")
.takes_value(true)
.default_value("1")
.value_name("SPACES"),
)
.get_matches();
let path = matches.value_of("file").unwrap();
let table = Table::from_path(path).unwrap_or_else(|e| {
eprintln!("Error parsing file: {}", e);
process::exit(1);
});
let style = matches.value_of("style").unwrap_or("basic");
let headers = matches.is_present("headers");
let separators = matches.is_present("separators");
let spaces = matches.value_of("spaces").unwrap().parse().unwrap_or(1);
let output = match style {
"basic" => table.basic_format(spaces),
"fancy" => table.fancy_format(headers, separators, spaces),
_ => unreachable!(),
};
match output {
Ok(output) => print!("{}", output),
Err(e) => {
eprintln!("Error formatting output: {}", e);
process::exit(1);
}
}
}