libtableformat/
table.rs

1mod border;
2pub mod row;
3pub mod cell;
4
5use std::str::FromStr;
6pub use border::Border;
7use super::data_item::DataItem;
8use cell::Cell;
9use row::Row;
10use crate::content::{ContentStyle, CellWidth};
11
12#[allow(unused_macros)]
13#[macro_export]
14macro_rules! table {
15    // Simple format
16    ( $($style:literal=>$header:literal),*; $($data:literal),* ) => {
17        table!(
18            $($style => $header),*;
19            "{}";
20            $($data),*
21        )
22    };
23
24    // Base cell style format
25    ( $($style:literal=>$header:literal),*;
26      $($cell_style:literal),*;
27      $($data:literal),* ) =>
28    {
29        Table::from_vec(
30            // Header specification
31            crate::row!($($style => $header), *),
32            // Base cell styles
33            &[$(crate::content_style!($cell_style)),*],
34            // Data
35            &[$($data),*]
36        )
37    }
38}
39
40#[derive(Debug)]
41pub struct Table {
42    pub border: Border,
43    column_breaks: Vec<CellWidth>,
44    column_headers: Row,
45    row_headers: Vec<Cell>,
46    data_rows: Vec<Row>
47}
48
49impl Table {
50    /// Returns an empty `Table`
51    #[must_use]
52    pub fn empty() -> Table {
53        Table {
54            border: Border::default(),
55            column_breaks: Vec::new(),
56            column_headers: Row::new(),
57            row_headers: Vec::new(),
58            data_rows: Vec::new(),
59        }
60    }
61
62    /// Returns a table from the supplied parameters.
63    ///
64    /// # Arguments
65    ///
66    /// * `border` - Describes the table border.
67    /// * `column_breaks` - Column breaks describe header row widths.
68    /// * `column_headers` - The content for the column headers.
69    /// * `row_headers` - The content for the row headers.
70    /// * `data_rows` - The rows in the table body.
71    #[must_use]
72    pub fn new(
73        border: Border,
74        column_breaks: Vec<CellWidth>,
75        column_headers: Row,
76        row_headers: Vec<Cell>,
77        data_rows: Vec<Row>,
78    ) -> Table {
79        Table {
80            border,
81            column_breaks,
82            column_headers,
83            row_headers,
84            data_rows
85        }
86    }
87
88    /// Returns a table built from a string vector data source.
89    ///
90    /// # Arguments
91    ///
92    /// * `column_headers` - The header row describes how to split the data.
93    /// * `cell_styles` - The base styles to apply to each cell.
94    /// * `data` - A vector containing the data for the table body.
95    ///
96    /// # Panics
97    ///
98    /// If a data item cannot be parsed.
99    #[must_use]
100    pub fn from_vec(
101        column_headers: Row,
102        cell_styles: &[ContentStyle],
103        data: &[&str]
104    ) -> Table {
105        // Build data items from string vector source
106        let d: Vec<DataItem> = 
107            data.iter().map(|i| DataItem::from_str(i).unwrap())
108                .collect::<Vec<DataItem>>();
109
110        Table::from_data_source(
111            column_headers,
112            &cell_styles,
113            Vec::new(),
114            d.iter()
115        )
116    }
117
118    /// Returns a table built from a data source.
119    ///
120    /// # Arguments
121    ///
122    /// * `column_headers` - The header row describes how to split the data.
123    /// * `cell_styles` - The base styles to apply to each cell.
124    /// * `row_headers` - The row headers to put before each row.
125    /// * `data_source` - An iterable source providing the table body data.
126    pub fn from_data_source<'a, I>(
127        column_headers: Row,
128        cell_styles: &[ContentStyle],
129        row_headers: Vec<Cell>,
130        data_source: I,
131    ) -> Table 
132        where 
133            I: Iterator<Item=&'a DataItem>
134    {
135        let mut data_rows = Vec::new();
136
137        // Derive column breaks from column headers
138        let mut column_breaks: Vec<CellWidth> = Vec::new();
139        for cell in column_headers.iter() {
140            column_breaks.push(cell.get_cell_width());
141        }
142
143        // Create a new row
144        let mut row_ix = 0;
145        data_rows.push(Row::new());
146
147        let mut break_ix = 0;
148
149        for item in data_source {
150            // Add a new row if needed
151            if break_ix == column_breaks.len() {
152                break_ix = 0;
153                data_rows.push(Row::new());
154                row_ix += 1;
155            }
156
157            // Get the cell style
158            let mut cell_style = &ContentStyle::default();
159            if cell_styles.len() > break_ix {
160                cell_style = &cell_styles[break_ix];
161            }
162
163            data_rows[row_ix].add_cell(
164                Cell::from_data_item(item, cell_style.clone())
165            );
166
167            break_ix += 1;
168        }
169
170        Table::new(
171            Border::default(),
172            column_breaks,
173            column_headers,
174            row_headers,
175            data_rows
176        )
177    }
178
179    /// Returns the contents of a table formatted as a string.
180    ///
181    /// # Arguments
182    ///
183    /// * `self` - The table to format.
184    #[must_use]
185    pub fn format(self: &Table) -> String {
186        let mut result: String = String::from("");
187
188        // Measure column widths
189        let widths = self.measure_column_widths();
190
191        // Format header row
192        result.push_str(&self.format_header(&widths));
193
194        // Format table body
195        result.push_str(&self.format_body(&widths));
196
197        result
198    }
199
200    /// Formats the table's column headers.
201    ///
202    /// # Arguments
203    ///
204    /// * `self` - The table containing the column headers to format.
205    fn format_header(
206        self: &Table,
207        widths: &[usize]
208    ) -> String {
209        let mut result: String = String::from("");
210
211        // Print top border
212        result.push_str(&self.border.format_top(&widths));
213        result.push('\n');
214
215        // Render column header row
216        result.push_str(
217            &self.column_headers.format(
218                &self.border,
219                &self.column_breaks
220            )
221        );
222
223        // Print horizontal split beneath headers
224        result.push_str(&self.border.format_horizontal_split(&widths));
225        result.push('\n');
226
227        result
228    }
229
230    /// Formats the body of a table.
231    ///
232    /// The specified `width` describes a desired output size and will be the
233    ///  maximum size of the formatted output. However, the table may also be
234    ///  formatted to a shorter width if there are insufficient column widths
235    ///  available to justify the full value.
236    ///
237    /// # Arguments
238    ///
239    /// * `self` - The table being formatted.
240    /// * `maximum_width` - The maximum render width, in chars.
241    fn format_body(
242        self: &Table,
243        widths: &[usize]
244    ) -> String {
245        let mut result: String = String::from("");
246
247        // Iterate rows
248        for row_ix in 0..self.data_rows.len() {
249            let row = &self.data_rows[row_ix];
250            result.push_str(
251                &row.format(
252                    &self.border,
253                    &self.column_breaks
254                )
255            );
256
257            // Print horizontal split beneath all but last row
258            if row_ix < self.data_rows.len() - 1 {
259                result.push_str(
260                    &self.border.format_horizontal_split(&widths));
261                result.push('\n');
262            }
263        }
264
265        // Print bottom border at end of table
266        result.push_str(&self.border.format_bottom(&widths));
267        result.push('\n');
268
269        result
270    }
271
272    /// Measures the widths of the columns of a table.
273    ///
274    /// Column breaks are used to constrain the render width of columns and
275    ///  are considered along with the content of the header cells.
276    ///
277    /// # Arguments
278    ///
279    /// * `self` - The table being measured.
280    fn measure_column_widths(
281        self: &Table
282    ) -> Vec<usize> {
283        let mut widths = Vec::new();
284
285        // Iterate through the header row
286        let content_break = CellWidth::Content;
287        for (column_break_ix, cell) in self.column_headers.iter().enumerate() {
288            // Get the next column break (if one is available)
289            let column_break: &CellWidth =
290                if column_break_ix < self.column_breaks.len() {
291                    &self.column_breaks[column_break_ix]
292                } else {
293                    // Use content-width break for additional columns
294                    &content_break
295                };
296            // Calculate the width of this header cell
297            widths.push(cell.measure_width(column_break));
298        }
299
300        widths
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use std::env;
308
309    /// Tests the simple format table! macro.
310    ///
311    /// This macro takes column breaks and header content in the first row, 
312    /// terminated by a semicolon.
313    ///
314    /// The second row is a vector of strings that are used for the table body.
315    #[test]
316    fn table_macro_simple_unstyled_body() {
317        let table = table!(
318            "{B^:12:}" => "Food", "{G^:7:}" => "Count";
319            "Fish", "15", "Pizza", "10", "Tomato", "24"
320        );
321
322        let expected =
323            match env::var("NO_COLOR") {
324                Ok(_) => "+------------+-------+\n|    Food    | Count |\n+------------+-------+\n|Fish        |15     |\n+------------+-------+\n|Pizza       |10     |\n+------------+-------+\n|Tomato      |24     |\n+------------+-------+\n",
325                Err(_) => "+------------+-------+\n|\u{1b}[94m    Food    \u{1b}[0m|\u{1b}[92m Count \u{1b}[0m|\n+------------+-------+\n|Fish        |15     |\n+------------+-------+\n|Pizza       |10     |\n+------------+-------+\n|Tomato      |24     |\n+------------+-------+\n",
326            };
327
328        assert_eq!(
329            table.format(),
330            expected
331        );
332    }
333
334    #[test]
335    fn table_macro_simple_styled_body() {
336        let table = table!(
337            "{m>:10:}" => "Item", "{m>:10:}" => "Price";
338            "{c^}", "{g<}";
339            "Basic", "$5,000", "Super", "$12,000", "Ultimate", "$35,000"
340        );
341
342        let expected =
343            match env::var("NO_COLOR") {
344                Ok(_) => "+----------+----------+\n|      Item|     Price|\n+----------+----------+\n|  Basic   |$5,000    |\n+----------+----------+\n|  Super   |$12,000   |\n+----------+----------+\n| Ultimate |$35,000   |\n+----------+----------+\n",
345                Err(_) => "+----------+----------+\n|\u{1b}[35m      Item\u{1b}[0m|\u{1b}[35m     Price\u{1b}[0m|\n+----------+----------+\n|\u{1b}[36m  Basic   \u{1b}[0m|\u{1b}[32m$5,000    \u{1b}[0m|\n+----------+----------+\n|\u{1b}[36m  Super   \u{1b}[0m|\u{1b}[32m$12,000   \u{1b}[0m|\n+----------+----------+\n|\u{1b}[36m Ultimate \u{1b}[0m|\u{1b}[32m$35,000   \u{1b}[0m|\n+----------+----------+\n"
346            };
347
348        assert_eq!(
349            table.format(),
350            expected
351        );
352    }
353}