1pub struct TableRenderer;
2
3impl TableRenderer {
4 const CHAR_CELL_SEPARATOR: char = '│';
5 const CHAR_LINE_SEPARATOR: char = '─';
6 const CHAR_JOIN_INNER: char = '┼';
7 const CHAR_CORNER_TOP_LEFT: char = '┌';
8 const CHAR_CORNER_TOP_RIGHT: char = '┐';
9 const CHAR_JOIN_LEFT_INNER: char = '├';
10 const CHAR_JOIN_RIGHT_INNER: char = '┤';
11 const CHAR_JOIN_TOP_INNER: char = '┬';
12 const CHAR_JOIN_BOTTOM_INNER: char = '┴';
13 const CHAR_CORNER_BOTTOM_LEFT: char = '└';
14 const CHAR_CORNER_BOTTOM_RIGHT: char = '┘';
15
16 pub fn render(data: &[Vec<String>]) -> String {
36 if data.is_empty() {
37 return String::new();
38 }
39
40 let (headers, rows) = data.split_first().unwrap();
41 let column_widths = Self::calculate_column_widths(headers, rows);
42
43 let header = Self::render_row(
44 headers,
45 &column_widths,
46 Self::CHAR_JOIN_TOP_INNER,
47 Self::CHAR_CORNER_TOP_LEFT,
48 Self::CHAR_CORNER_TOP_RIGHT,
49 );
50
51 let rows: Vec<String> = rows
52 .iter()
53 .map(|row| {
54 Self::render_row(
55 row,
56 &column_widths,
57 Self::CHAR_JOIN_INNER,
58 Self::CHAR_JOIN_LEFT_INNER,
59 Self::CHAR_JOIN_RIGHT_INNER,
60 )
61 })
62 .collect();
63
64 let footer = Self::render_separator(
65 &column_widths,
66 Self::CHAR_JOIN_BOTTOM_INNER,
67 Self::CHAR_CORNER_BOTTOM_LEFT,
68 Self::CHAR_CORNER_BOTTOM_RIGHT,
69 );
70
71 format!("{}{}{}", header, rows.join(""), footer)
72 }
73
74 fn calculate_column_widths(headers: &[String], rows: &[Vec<String>]) -> Vec<usize> {
75 let mut column_widths: Vec<usize> = vec![];
76 for (i, header) in headers.iter().enumerate() {
77 let mut max_width = header.chars().count();
78 for row in rows {
79 if let Some(cell) = row.get(i) {
80 max_width = max_width.max(cell.chars().count());
81 }
82 }
83 column_widths.push(max_width + 2); }
85 column_widths
86 }
87
88 fn render_row(
89 row: &[String],
90 column_widths: &[usize],
91 join_inner: char,
92 corner_left: char,
93 corner_right: char,
94 ) -> String {
95 let cells: Vec<String> = column_widths
96 .iter()
97 .enumerate()
98 .map(|(i, &width)| {
99 let empty_string = "".to_string();
100 let cell_content = row.get(i).unwrap_or(&empty_string);
101 let padded_content = format!(" {} ", cell_content);
102 format!("{:width$}", padded_content, width = width)
103 })
104 .collect();
105
106 let line = format!(
107 "{}{}{}\n",
108 Self::CHAR_CELL_SEPARATOR,
109 cells.join(&Self::CHAR_CELL_SEPARATOR.to_string()),
110 Self::CHAR_CELL_SEPARATOR
111 );
112
113 let separator =
114 Self::render_separator(column_widths, join_inner, corner_left, corner_right);
115
116 format!("{}{}", separator, line)
117 }
118
119 fn render_separator(
120 column_widths: &[usize],
121 join_inner: char,
122 corner_left: char,
123 corner_right: char,
124 ) -> String {
125 let separators: Vec<String> = column_widths
126 .iter()
127 .map(|&width| Self::CHAR_LINE_SEPARATOR.to_string().repeat(width))
128 .collect();
129
130 format!(
131 "{}{}{}\n",
132 corner_left,
133 separators.join(&join_inner.to_string()),
134 corner_right
135 )
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_table_rendering() {
145 let data = vec![
146 vec!["Header 1".to_string(), "Header 2".to_string()],
147 vec!["Row1".to_string(), "Row1Col2".to_string()],
148 ];
149 let rendered_table = TableRenderer::render(&data);
150 let expected_table = "\
151┌──────────┬──────────┐\n\
152│ Header 1 │ Header 2 │\n\
153├──────────┼──────────┤\n\
154│ Row1 │ Row1Col2 │\n\
155└──────────┴──────────┘\n";
156
157 assert_eq!(rendered_table, expected_table);
158 }
159}