csv_managed/
table.rs

1use std::fmt::Write as _;
2
3pub fn render_table(headers: &[String], rows: &[Vec<String>]) -> String {
4    let column_count = headers.len();
5    let mut widths = headers.iter().map(|h| display_width(h)).collect::<Vec<_>>();
6
7    for row in rows {
8        for (idx, cell) in row.iter().enumerate().take(column_count) {
9            widths[idx] = widths[idx].max(display_width(cell));
10        }
11    }
12
13    let mut output = String::new();
14
15    // Header
16    let header_line = format_row(headers, &widths);
17    let _ = writeln!(output, "{header_line}");
18
19    // Separator
20    let separator = widths
21        .iter()
22        .map(|w| "-".repeat(*w.max(&3) + 2))
23        .collect::<Vec<_>>()
24        .join("+");
25    let _ = writeln!(output, "{separator}");
26
27    // Rows
28    for row in rows {
29        let row_line = format_row(row, &widths);
30        let _ = writeln!(output, "{row_line}");
31    }
32
33    output
34}
35
36pub fn print_table(headers: &[String], rows: &[Vec<String>]) {
37    let rendered = render_table(headers, rows);
38    print!("{rendered}");
39}
40
41fn format_row(values: &[String], widths: &[usize]) -> String {
42    values
43        .iter()
44        .enumerate()
45        .map(|(idx, value)| format!(" {:<width$} ", value, width = widths[idx]))
46        .collect::<Vec<_>>()
47        .join("|")
48}
49
50fn display_width(value: &str) -> usize {
51    let mut width = 0usize;
52    let mut chars = value.chars();
53    while let Some(ch) = chars.next() {
54        if ch == '\u{1b}' {
55            // Skip ANSI escape sequence (e.g. \x1b[31m)
56            for next in chars.by_ref() {
57                if next == 'm' {
58                    break;
59                }
60            }
61        } else {
62            width += 1;
63        }
64    }
65    width
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn render_table_aligns_columns() {
74        let headers = vec!["id".to_string(), "name".to_string()];
75        let rows = vec![
76            vec!["1".to_string(), "Alice".to_string()],
77            vec!["2".to_string(), "Bob".to_string()],
78        ];
79
80        let rendered = render_table(&headers, &rows);
81        let lines: Vec<&str> = rendered.lines().collect();
82
83        assert_eq!(lines.len(), 4);
84        assert_eq!(lines[0], " id | name  ");
85        assert!(lines[1].contains("---"));
86        assert_eq!(lines[2], " 1  | Alice ");
87        assert_eq!(lines[3], " 2  | Bob   ");
88    }
89
90    #[test]
91    fn display_width_counts_characters() {
92        assert_eq!(display_width("abc"), 3);
93        assert_eq!(display_width(""), 0);
94        assert_eq!(display_width("résumé"), 6);
95    }
96
97    #[test]
98    fn display_width_ignores_ansi_sequences() {
99        let value = "\u{1b}[31minvalid\u{1b}[0m";
100        assert_eq!(display_width(value), "invalid".len());
101    }
102}