1const COLUMN_GAP: &str = " ";
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum ColumnAlign {
9 Left,
10 Right,
11}
12
13#[must_use]
15pub fn render_table<const N: usize>(
16 headers: &[&str; N],
17 rows: &[[String; N]],
18 alignments: &[ColumnAlign; N],
19) -> String {
20 let widths = table_widths(headers, rows);
21 let mut lines = Vec::with_capacity(rows.len() + 2);
22 lines.push(render_table_row(headers, &widths, alignments));
23 lines.push(render_separator(&widths));
24 lines.extend(
25 rows.iter()
26 .map(|row| render_table_row(row, &widths, alignments)),
27 );
28 lines.join("\n")
29}
30
31#[must_use]
33pub fn table_widths<const N: usize>(headers: &[&str; N], rows: &[[String; N]]) -> [usize; N] {
34 let mut widths = headers.map(str::chars).map(Iterator::count);
35
36 for row in rows {
37 for (index, cell) in row.iter().enumerate() {
38 widths[index] = widths[index].max(cell.chars().count());
39 }
40 }
41
42 widths
43}
44
45#[must_use]
47pub fn render_table_row<const N: usize>(
48 row: &[impl AsRef<str>],
49 widths: &[usize; N],
50 alignments: &[ColumnAlign; N],
51) -> String {
52 widths
53 .iter()
54 .zip(alignments)
55 .enumerate()
56 .map(|(index, (width, alignment))| {
57 let value = row.get(index).map_or("", AsRef::as_ref);
58 match alignment {
59 ColumnAlign::Left => format!("{value:<width$}"),
60 ColumnAlign::Right => format!("{value:>width$}"),
61 }
62 })
63 .collect::<Vec<_>>()
64 .join(COLUMN_GAP)
65 .trim_end()
66 .to_string()
67}
68
69#[must_use]
71pub fn render_separator<const N: usize>(widths: &[usize; N]) -> String {
72 widths
73 .iter()
74 .map(|width| "-".repeat(*width))
75 .collect::<Vec<_>>()
76 .join(COLUMN_GAP)
77}