use std::{cmp, io, io::Write};
pub struct Table<'t, 's> {
titles: Option<Vec<&'t str>>,
rows: Vec<Vec<String>>,
delim_str: &'s str,
is_row_count_enabled: bool,
}
impl<'t> Table<'t, '_> {
pub fn new() -> Self {
Self {
titles: None,
rows: Vec::new(),
delim_str: "|",
is_row_count_enabled: false,
}
}
pub fn set_titles(&mut self, titles: Vec<&'t str>) {
self.titles = Some(titles);
}
pub fn enable_row_count(&mut self) -> &mut Self {
self.is_row_count_enabled = true;
self
}
pub fn add_row(&mut self, row: Vec<String>) {
self.rows.push(row);
}
pub fn render<T: Write>(&self, out: &mut T) -> io::Result<()> {
self.render_titles(out)?;
out.write_all(b"\n")?;
self.render_separator(out)?;
if !self.rows.is_empty() {
out.write_all(b"\n")?;
self.render_rows(out)?;
out.write_all(b"\n")?;
}
if self.is_row_count_enabled {
out.write_all(format!("\n{} row(s)\n", self.rows.len()).as_bytes())?;
}
Ok(())
}
pub fn print_stdout(&self) {
let mut stdout = io::stdout();
self.render(&mut stdout).unwrap();
stdout.flush().unwrap();
}
fn col_width(&self, idx: usize) -> usize {
let title_width = self
.titles
.as_ref()
.map(|titles| titles.get(idx).map(|s| s.len()).unwrap_or_default())
.unwrap_or_default();
let rows_width = self.rows.iter().fold(0, |max, r| {
if idx < r.len() {
cmp::max(max, r.get(idx).expect("Already checked").len())
} else {
max
}
});
cmp::max(title_width, rows_width)
}
fn render_titles<T: Write>(&self, out: &mut T) -> io::Result<()> {
if let Some(titles) = self.titles.as_ref() {
self.render_row(titles, out)?;
}
Ok(())
}
fn render_rows<T: Write>(&self, out: &mut T) -> io::Result<()> {
let rows_len = self.rows.len();
for (i, row) in self.rows.iter().enumerate() {
self.render_row(row, out)?;
if i < rows_len - 1 {
out.write_all(b"\n")?;
}
}
Ok(())
}
fn render_row<T: Write, I: AsRef<[S]>, S: ToString>(&self, row: I, out: &mut T) -> io::Result<()> {
let row_len = row.as_ref().len();
for (i, string) in row.as_ref().iter().enumerate() {
let s = string.to_string();
let width = self.col_width(i);
let pad_left = if i == 0 { "" } else { " " };
let pad_right = " ".repeat(width - s.len() + 1);
out.write_all(pad_left.as_bytes())?;
out.write_all(s.as_bytes())?;
out.write_all(pad_right.as_bytes())?;
if i < row_len - 1 {
out.write_all(self.delim_str.as_bytes())?;
}
}
Ok(())
}
fn render_separator<T: Write>(&self, out: &mut T) -> io::Result<()> {
if let Some(rows_len) = self.rows.first().map(|r| r.len()) {
for i in 0..rows_len {
let width = self.col_width(i);
let pad_left = if i == 0 { "" } else { " " };
out.write_all(pad_left.as_bytes())?;
let sep = "-".repeat(width);
out.write_all(sep.as_bytes())?;
out.write_all(" ".as_bytes())?;
if i < rows_len - 1 {
out.write_all(self.delim_str.as_bytes())?;
}
}
}
Ok(())
}
}
macro_rules! row {
($($s:expr),*$(,)?) => {
vec![$($s.to_string()),*]
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn renders_titles() {
let mut table = Table::new();
table.set_titles(vec!["Hello", "World", "Bonjour", "Le", "Monde"]);
let mut buf = io::Cursor::new(Vec::new());
table.render(&mut buf).unwrap();
assert_eq!(
String::from_utf8_lossy(&buf.into_inner()),
"Hello | World | Bonjour | Le | Monde \n"
);
}
#[test]
fn renders_rows_with_titles() {
let mut table = Table::new();
table.set_titles(vec!["Name", "Age", "Telephone Number", "Favourite Headwear"]);
table.add_row(row!["Trevor", 132, "+123 12323223", "Pith Helmet"]);
table.add_row(row![]);
table.add_row(row!["Hatless", 2]);
let mut buf = io::Cursor::new(Vec::new());
table.render(&mut buf).unwrap();
assert_eq!(
String::from_utf8_lossy(&buf.into_inner()),
"Name | Age | Telephone Number | Favourite Headwear \n------- | --- | ---------------- | \
------------------ \nTrevor | 132 | +123 12323223 | Pith Helmet \n\nHatless | 2 \n"
);
}
}