minimal_table/
lib.rs

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    /// Renders the given data as a string representing a table.
17    ///
18    /// The first row of `data` is treated as the header.
19    ///
20    /// # Arguments
21    ///
22    /// * `data` - A slice of `Vec<String>` where each `Vec<String>` represents a row in the table.
23    ///
24    /// # Examples
25    ///
26    /// ```
27    /// use minimal_table::TableRenderer;
28    ///
29    /// let data = vec![
30    ///     vec!["Header 1".to_string(), "Header 2".to_string()],
31    ///     vec!["Row1".to_string(), "Row1Col2".to_string()],
32    /// ];
33    /// println!("{}", TableRenderer::render(&data));
34    /// ```
35    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); // +2 for padding
84        }
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}