1#[derive(Clone, Debug, Eq, PartialEq)]
6pub struct WhitespaceTable {
7 headers: Vec<String>,
8 rows: Vec<Vec<String>>,
9}
10
11impl WhitespaceTable {
12 #[must_use]
14 pub fn new(headers: impl IntoIterator<Item = impl Into<String>>) -> Self {
15 Self {
16 headers: headers.into_iter().map(Into::into).collect(),
17 rows: Vec::new(),
18 }
19 }
20
21 pub fn push_row(&mut self, row: impl IntoIterator<Item = impl Into<String>>) {
23 self.rows.push(row.into_iter().map(Into::into).collect());
24 }
25
26 #[must_use]
28 pub fn render(&self) -> String {
29 let widths = self.column_widths();
30 let mut lines = Vec::with_capacity(self.rows.len() + 1);
31 lines.push(render_row(&self.headers, &widths));
32 lines.extend(self.rows.iter().map(|row| render_row(row, &widths)));
33 lines.join("\n")
34 }
35
36 fn column_widths(&self) -> Vec<usize> {
38 (0..self.headers.len())
39 .map(|index| {
40 std::iter::once(self.headers.get(index).map_or("", String::as_str))
41 .chain(
42 self.rows
43 .iter()
44 .map(move |row| row.get(index).map_or("", String::as_str)),
45 )
46 .map(display_width)
47 .max()
48 .unwrap_or(0)
49 })
50 .collect()
51 }
52}
53
54fn render_row(row: &[String], widths: &[usize]) -> String {
56 widths
57 .iter()
58 .enumerate()
59 .map(|(index, width)| {
60 let value = row.get(index).map_or("", String::as_str);
61 format!("{value:<width$}")
62 })
63 .collect::<Vec<_>>()
64 .join(" ")
65 .trim_end()
66 .to_string()
67}
68
69fn display_width(value: &str) -> usize {
71 value.chars().count()
72}