unicode_prettytable/
table.rs

1use crate::util::StringBuffer;
2
3use std::fmt;
4use derive_builder::Builder;
5
6// regular derive(Default) requires T: Default for Option<T>
7use smart_default::SmartDefault;
8
9// https://www.unicode.org/charts/PDF/U2500.pdf
10const VERTICAL: &str = "\u{2502}"; // │
11
12const HORIZONTAL: &str = "\u{2500}"; // ─
13const HORIZONTAL_HEADER: &str = "\u{2550}"; // ═
14
15const TOP_BRACE: &str = "\u{252C}"; // ┬
16const TOP_BRACE_HEADER: &str = "\u{2564}"; // ╤
17
18const BOTTOM_BRACE: &str = "\u{2534}"; // ┴
19const LEFT_BRACE: &str = "\u{251c}"; // ├
20const LEFT_BRACE_HEADER: &str = "\u{255e}"; // ╞
21const RIGHT_BRACE: &str = "\u{2524}"; // ┤
22const RIGHT_BRACE_HEADER: &str = "\u{2561}"; // ╡
23const MIDDLE_BRACE: &str = "\u{253C}"; // ┼
24const MIDDLE_BRACE_HEADER: &str = "\u{256a}"; // ╪
25
26const TOP_LEFT_CORNER: &str = "\u{250c}"; // ┌
27const TOP_RIGHT_CORNER: &str = "\u{2510}"; // ┐
28
29const TOP_LEFT_CORNER_HEADER: &str = "\u{2552}"; // ╒
30const TOP_RIGHT_CORNER_HEADER: &str = "\u{2555}"; // ╕
31
32const BOTTOM_RIGHT_CORNER: &str = "\u{2518}"; // ┘
33const BOTTOM_LEFT_CORNER: &str = "\u{2514}"; // └
34
35#[derive(Builder, Clone, SmartDefault)]
36pub struct Header<'a, T>
37where
38    T: AsRef<str>,
39    &'a T: AsRef<str>
40{
41    /// Whether to use double bar Unicode characters surrounding the header
42    double_bar: bool,
43    /// Whether to center the header text within each column
44    centered_text: bool,
45    /// Use a separate set of columns for the header
46    /// Otherwise, the header will automatically use the first row in the table input
47    #[builder(setter(strip_option), default)]
48    columns: Option<&'a Vec<T>>
49}
50
51#[derive(Builder)]
52pub struct Table<'a, T>
53where
54    T: AsRef<str>,
55    &'a T: AsRef<str>,
56{
57    /// If you provide header settings, the first row will be treated as headers
58    #[builder(default)]
59    header: Header<'a, T>,
60    /// Rows holding the data
61    rows: &'a Vec<Vec<T>>,
62}
63
64impl <'a, T> Table<'a, T>
65where
66    T: AsRef<str>,
67    &'a T: AsRef<str>
68{
69    /// Creates a list of byte vectors corresponding to each string of horizontal separators
70    fn generate_horizontal_separators(column_widths: &Vec<usize>, horizontal_char: &str) -> Vec<Vec<u8>> {
71        column_widths
72            .iter()
73            .map(|&length| horizontal_char.repeat(length).into_bytes())
74            .collect::<Vec<_>>()
75    }
76
77    /// Given a 2D input, returns the minimum width of each column in a vector
78    fn get_column_widths(&self, header_columns: &Vec<T>, row_offset: usize) -> Vec<usize>
79    where
80        T: AsRef<str>,
81        &'a T: AsRef<str>
82    {
83        let header_widths: Vec<usize> = {
84            let header_padding = {
85                if self.header.centered_text {
86                    2
87                }
88                else {
89                    0
90                }
91            };
92
93            header_columns.iter().map(|entry| entry.as_ref().chars().count() + header_padding).collect()
94        };
95
96        let mut widths = self.rows
97            .iter()
98            .map(|row| row
99                .into_iter()
100                .map(|entry| entry.as_ref().chars().count())
101            )
102            // doesn't waste time looking at the first row if header_columns is pointing to
103            // self.rows
104            // then, header_widths would have already factored it in
105            .skip(row_offset)
106            .fold(header_widths.clone(), |mut column_widths: Vec<usize>, row| {
107                for (a, b) in column_widths.iter_mut().zip(row) {
108                    // if b is bigger than the current column width, set the new column width to b
109                    *a = b.max(*a);
110                }
111
112                column_widths
113            });
114
115        if self.header.centered_text {
116            // make sure that header has an even number of spaces on either side
117            for (col_width, header_width) in widths.iter_mut().zip(header_widths.into_iter()) {
118                let num_spaces = *col_width - header_width;
119
120                // let there be a space on either side
121                if num_spaces == 0 {
122                    *col_width += 2;
123                }
124                // make it an even number of spaces
125                else if num_spaces % 2 == 1 {
126                    *col_width += 1;
127                }
128            }
129        }
130
131        widths
132    }
133}
134
135impl <'a, T> fmt::Display for Table<'a, T>
136where
137    T: AsRef<str>,
138    &'a T: AsRef<str>,
139{
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        let (header, row_offset) = {
142            if let Some(columns) = self.header.columns {
143                (columns, 0)
144            }
145            else if let Some(columns) = self.rows.get(0) {
146                (columns, 1)
147            }
148            else {
149                // returns an error when there are no rows
150                return Err(fmt::Error::default());
151            }
152        };
153
154        let column_widths = self.get_column_widths(&header, row_offset);
155
156        let total_width_per_row = {
157            // one separator to the left of each one, as well as one separator on the very right
158            let num_separators_per_row = column_widths.len() + 1;
159
160            let base_width_per_row: usize = column_widths.iter().sum();
161
162            base_width_per_row + num_separators_per_row
163        };
164
165        let string_length = {
166            let total_lines = {
167                let num_rows = self.rows.len();
168
169                // separator above each row PLUS one under the last row
170                let num_separator_lines = num_rows + 1;
171
172                num_separator_lines + num_rows
173            };
174
175            // no newline after the last line
176            let num_newlines = total_lines - 1;
177
178            (total_width_per_row * total_lines) + num_newlines
179        };
180
181        // fill string with spaces
182        let mut buffer = StringBuffer::with_capacity_fill(string_length, ' ');
183
184        let standard_horizontal_separators = Self::generate_horizontal_separators(&column_widths, HORIZONTAL);
185        let header_horizontal_separators = Self::generate_horizontal_separators(&column_widths, HORIZONTAL_HEADER);
186
187        let create_sep_row = |horizontal_separators: &Vec<Vec<u8>>, left_char, middle_char, right_char, newline| {
188            let mut sep_buffer = {
189                // allocate one extra byte if there is a newline at the end
190                let newline_increment = if newline { 1 } else { 0 };
191                let capacity = total_width_per_row + newline_increment;
192                StringBuffer::with_capacity(capacity)
193            };
194
195            sep_buffer.push_chars(left_char);
196
197            let (last_sep, seps) = horizontal_separators.split_last().unwrap();
198
199            for sep in seps {
200                sep_buffer.push_bytes(sep);
201                sep_buffer.push_chars(middle_char);
202            }
203
204            sep_buffer.push_bytes(&last_sep);
205            sep_buffer.push_chars(right_char);
206
207            if newline {
208                sep_buffer.push_chars("\n")
209            }
210
211            sep_buffer.into_buffer()
212        };
213
214        let mut input_iterator = self.rows.iter().skip(row_offset).peekable();
215
216        macro_rules! push_data_row {
217            ($row_yielder:expr, $col_formatter:expr) => {
218                if let Some(row) = $row_yielder {
219                    buffer.push_chars(VERTICAL);
220                    for (col_index, col) in row.into_iter().enumerate() {
221                        let base = col.as_ref();
222
223                        buffer.push_chars_fixed_width($col_formatter(base, column_widths[col_index]), column_widths[col_index]);
224                        buffer.push_chars(VERTICAL);
225                    }
226
227                    buffer.push_chars("\n");
228                }
229            };
230            // overload that defaults to using input_iterator
231            ($col_formatter:expr) => {
232                push_data_row!(input_iterator.next(), $col_formatter)
233            }
234        }
235
236        let standard_separator = create_sep_row(&standard_horizontal_separators, LEFT_BRACE, MIDDLE_BRACE, RIGHT_BRACE, true);
237
238        macro_rules! push_header_data_row {
239            () => (
240                if self.header.centered_text {
241                    // align header text in the center of each column
242                    push_data_row!(Some(header), |base_str, width| format!("{:^width$}", base_str, width = width));
243                }
244                else {
245                    push_data_row!(Some(header), |base_str, _| base_str);
246                }
247            )
248        }
249
250        if self.header.double_bar {
251            let header_top = create_sep_row(&header_horizontal_separators, TOP_LEFT_CORNER_HEADER, TOP_BRACE_HEADER, TOP_RIGHT_CORNER_HEADER, true);
252            buffer.push_bytes(&header_top);
253
254            push_header_data_row!();
255
256            let header_bottom = create_sep_row(&header_horizontal_separators, LEFT_BRACE_HEADER, MIDDLE_BRACE_HEADER, RIGHT_BRACE_HEADER, true);
257            buffer.push_bytes(&header_bottom);
258
259            // TODO
260            // handle case where there are no remaining rows
261            // currently, it will output an empty body
262        }
263        else {
264            let header = create_sep_row(&standard_horizontal_separators, TOP_LEFT_CORNER, TOP_BRACE, TOP_RIGHT_CORNER, true);
265            // add header
266            buffer.push_bytes(&header);
267            push_header_data_row!();
268
269            // only put the separator if there are rows under
270            if input_iterator.peek().is_some() {
271                buffer.push_bytes(&standard_separator);
272            }
273        }
274
275        let footer = create_sep_row(&standard_horizontal_separators, BOTTOM_LEFT_CORNER, BOTTOM_BRACE, BOTTOM_RIGHT_CORNER, false);
276
277        loop {
278            push_data_row!(|base_str, _| base_str);
279
280            // only create a standard separator row if there are more data rows
281            if input_iterator.peek().is_some() {
282                buffer.push_bytes(&standard_separator);
283            }
284            else {
285                // break out if there are no more data rows
286                break;
287            }
288        }
289
290        // add footer
291        buffer.push_bytes(&footer);
292
293        write!(f, "{}", buffer.to_string())
294    }
295}