mod config;
pub use config::*;
#[cfg(test)] mod test;
use std::fmt::Display;
const SE: &str = "┌";
const NW: &str = "┘";
const SW: &str = "┐";
const NS: &str = "│";
const NE: &str = "└";
const EWS: &str = "┬";
const NES: &str = "├";
const NWS: &str = "┤";
const NEW: &str = "┴";
const NEWS: &str = "┼";
const EW: &str = "─";
pub fn print_table<L1, L2, T>(data: L1, conf: &TableConfig)
where L1: IntoIterator<Item = L2>,
L2: IntoIterator<Item = T>,
T: Display {
print!("{}", format_table(data, conf))
}
pub fn format_table<L1, L2, T>(data: L1, conf: &TableConfig) -> String
where L1: IntoIterator<Item = L2>,
L2: IntoIterator<Item = T>,
T: Display {
format_table_inner(stringify(data), conf)
}
fn format_table_inner(data: Vec<Vec<String>>, conf: &TableConfig) -> String {
if !valid(&data, conf) {
return format_empty()
}
let num_cols = data.iter().map(|x| x.len()).max().unwrap();
let data = square_data(data, num_cols);
let conf = correct_config(conf, num_cols);
let header = conf.columns.iter().any(|(_, x)| x.header.chars().count() > 0);
let widths = column_widths(&data, &conf);
let mut result = String::new();
result.push_str(&format_first(&widths));
if header {
let x: Vec<_> = conf.columns.iter().map(|(_, x)| x.header.clone()).collect();
result.push_str(&format_row2(&x, &widths));
result.push_str(&format_middle(&widths));
}
for row in data {
result.push_str(&format_row(&row, &conf, &widths));
}
result.push_str(&format_last(&widths));
result
}
fn valid(data: &[Vec<String>], conf: &TableConfig) -> bool {
if data.len() == 0 {
false
} else if conf.width < 4 {
false
} else if data.iter().map(|x| x.len()).max().unwrap_or(0) == 0 {
false
} else {
true
}
}
fn stringify<L1, L2, T>(data: L1) -> Vec<Vec<String>>
where L1: IntoIterator<Item = L2>,
L2: IntoIterator<Item = T>,
T: Display {
data.into_iter().map(|row| row.into_iter().map(|cell| cell.to_string()).collect()).collect()
}
fn square_data(mut data: Vec<Vec<String>>, num_cols: usize) -> Vec<Vec<String>> {
for row in data.iter_mut() {
while row.len() < num_cols {
row.push(String::new())
}
}
data
}
fn correct_config(conf: &TableConfig, num_cols: usize) -> TableConfig {
let mut conf = conf.clone();
for col in 0..num_cols {
if conf.columns.get(&col).is_none() {
conf.columns.insert(col, ColumnConfig::default());
}
}
conf.columns.split_off(&num_cols);
conf
}
fn column_widths(data: &[Vec<String>], conf: &TableConfig) -> Vec<usize> {
let result: Vec<_> = (0..conf.columns.len()).map(|a| {
let column_width = data.iter().map(|row| row[a].chars().count()).max().unwrap();
let header_width = conf.columns[&a].header.chars().count();
column_width.max(header_width)
}).collect();
truncate_widths(result, conf)
}
fn truncate_widths(mut widths: Vec<usize>, conf: &TableConfig) -> Vec<usize> {
let max_width = conf.width;
let table_padding = ((widths.len() - 1) * 3) + 4;
while widths.iter().sum::<usize>() + table_padding > max_width &&
*widths.iter().max().unwrap() > 0 {
let max = widths.iter().max().unwrap();
let idx = widths.iter().rposition(|x| x == max).unwrap();
widths[idx] -= 1;
}
widths
}
fn format_line(row: &[String], head: &str, delim: &str, tail: &str) -> String {
let mut result = String::new();
result.push_str(head);
for cell in row {
result.push_str(&format!("{}{}", cell, delim));
}
for _ in 0..delim.chars().count() {
result.pop();
}
result.push_str(tail);
result.push('\n');
result
}
fn format_empty() -> String {
format_first(&vec![0])
+ &format_line(&vec![String::new()], &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
+ &format_last(&vec![0])
}
fn format_first(widths: &[usize]) -> String {
let row: Vec<String> = widths.iter().map(|&x| EW.repeat(x)).collect();
format_line(&row, &format!("{}{}", SE, EW), &format!("{}{}{}", EW, EWS, EW), &format!("{}{}", EW, SW))
}
fn format_middle(widths: &[usize]) -> String {
let row: Vec<String> = widths.iter().map(|&x| EW.repeat(x)).collect();
format_line(&row, &format!("{}{}", NES, EW), &format!("{}{}{}", EW, NEWS, EW), &format!("{}{}", EW, NWS))
}
fn format_row(row: &[String], conf: &TableConfig, widths: &[usize]) -> String {
let row: Vec<String> = row.iter().zip(widths.iter()).zip(conf.columns.iter()).map(|((cell, &width), (_, conf))|
make_cell(&cell, width, ' ', conf.align)
).collect();
format_line(&row, &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
}
fn format_row2(row: &[String], widths: &[usize]) -> String {
let row: Vec<String> = row.iter().zip(widths.iter()).map(|(cell, &width)|
make_cell(&cell, width, ' ', Align::Left)
).collect();
format_line(&row, &format!("{}{}", NS, ' '), &format!("{}{}{}", ' ', NS, ' '), &format!("{}{}", ' ', NS))
}
fn format_last(widths: &[usize]) -> String {
let row: Vec<String> = widths.iter().map(|&x| EW.repeat(x)).collect();
format_line(&row, &format!("{}{}", NE, EW), &format!("{}{}{}", EW, NEW, EW), &format!("{}{}", EW, NW))
}
fn make_cell(text: &str, len: usize, pad: char, align: Align) -> String {
if text.chars().count() > len {
let mut result: String = text.chars().take(len).collect();
if result.pop().is_some() {
result.push('+')
}
result
} else {
let mut result = text.to_string();
match align {
Align::Left => while result.chars().count() < len {
result.push(pad)
}
Align::Right => while result.chars().count() < len {
result.insert(0, pad)
}
Align::Center => while result.chars().count() < len {
result.push(pad);
if result.chars().count() < len {
result.insert(0, pad)
}
}
}
result
}
}