tabular/
table.rs

1// Copyright (c) tabular-rs Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    column_spec::{parse_row_spec, row_spec_to_string, ColumnSpec},
6    error::Result,
7    row::{InternalRow, Row},
8    width_string::WidthString,
9};
10
11use std::fmt::{Debug, Display, Formatter};
12
13/// Builder type for constructing a formatted table.
14///
15/// Construct this with [`Table::new()`] or [`Table::new_safe()`]. Then add rows
16/// to it with [`Table::add_row()`] and [`Table::add_heading()`].
17///
18/// [`Table::new_safe()`]: struct.Table.html#method.new_safe
19/// [`Table::new()`]: struct.Table.html#method.new
20/// [`Table::add_row()`]: struct.Table.html#method.add_row
21/// [`Table::add_heading()`]: struct.Table.html#method.add_heading
22#[derive(Clone)]
23pub struct Table {
24    n_columns: usize,
25    format: Vec<ColumnSpec>,
26    rows: Vec<InternalRow>,
27    column_widths: Vec<usize>,
28    line_end: String,
29}
30
31const DEFAULT_LINE_END: &str = "\n";
32
33impl Table {
34    /// Constructs a new table with the format of each row specified by `row_spec`.
35    ///
36    /// Unlike `format!` and friends, `row_spec` is processed dynamically, but it uses a small
37    /// subset of the syntax to determine how columns are laid out. In particular:
38    ///
39    ///   - `{:<}` produces a left-aligned column.
40    ///
41    ///   - `{:^}` produces a centered column.
42    ///
43    ///   - `{:>}` produces a right-aligned column.
44    ///
45    ///   - `{{` produces a literal `{` character.
46    ///
47    ///   - `}}` produces a literal `}` character.
48    ///
49    ///   - Any other appearances of `{` or `}` are errors.
50    ///
51    ///   - Everything else stands for itself.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// # use tabular::*;
57    /// let table = Table::new("{{:<}} produces ‘{:<}’ and {{:>}} produces ‘{:>}’")
58    ///     .with_row(Row::from_cells(["a", "bc"].iter().cloned()));
59    /// ```
60    pub fn new(row_spec: &str) -> Self {
61        Self::new_safe(row_spec)
62            .unwrap_or_else(|e: super::error::Error| panic!("tabular::Table::new: {}", e))
63    }
64
65    /// Like [`new`], but returns a [`Result`] instead of panicking if parsing `row_spec` fails.
66    ///
67    /// [`new`]: #method.new
68    /// [`Result`]: type.Result.html
69    pub fn new_safe(row_spec: &str) -> Result<Self> {
70        let (format, n_columns) = parse_row_spec(row_spec)?;
71
72        Ok(Table {
73            n_columns,
74            format,
75            rows: vec![],
76            column_widths: vec![0; n_columns],
77            line_end: DEFAULT_LINE_END.to_owned(),
78        })
79    }
80
81    /// The number of columns in the table.
82    pub fn column_count(&self) -> usize {
83        // ^^^^^^^^^^^^ What’s a better name for this?
84        self.n_columns
85    }
86
87    /// Adds a pre-formatted row that spans all columns.
88    ///
89    /// A heading does not interact with the formatting of rows made of cells.
90    /// This is like `\intertext` in LaTeX, not like `<head>` or `<th>` in HTML.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// # use tabular::*;
96    ///         let mut table = Table::new("{:<}  {:>}");
97    ///         table
98    ///             .add_heading("./:")
99    ///             .add_row(Row::new().with_cell("Cargo.lock").with_cell(433))
100    ///             .add_row(Row::new().with_cell("Cargo.toml").with_cell(204))
101    ///             .add_heading("")
102    ///             .add_heading("src/:")
103    ///             .add_row(Row::new().with_cell("lib.rs").with_cell(10257))
104    ///             .add_heading("")
105    ///             .add_heading("target/:")
106    ///             .add_row(Row::new().with_cell("debug/").with_cell(672));
107    ///
108    ///         assert_eq!( format!("{}", table),
109    ///                     "./:\n\
110    ///                      Cargo.lock    433\n\
111    ///                      Cargo.toml    204\n\
112    ///                      \n\
113    ///                      src/:\n\
114    ///                      lib.rs      10257\n\
115    ///                      \n\
116    ///                      target/:\n\
117    ///                      debug/        672\n\
118    ///                      " );
119    /// ```
120    ///
121    pub fn add_heading<S: Into<String>>(&mut self, heading: S) -> &mut Self {
122        self.rows.push(InternalRow::Heading(heading.into()));
123        self
124    }
125
126    /// Convenience function for calling [`add_heading`].
127    ///
128    /// [`add_heading`]: #method.add_heading
129    #[must_use]
130    pub fn with_heading<S: Into<String>>(mut self, heading: S) -> Self {
131        self.add_heading(heading);
132        self
133    }
134
135    /// Adds a row made up of cells.
136    ///
137    /// When printed, each cell will be padded to the size of its column, which is the maximum of
138    /// the width of its cells.
139    ///
140    /// # Panics
141    ///
142    /// If `self.`[`column_count()`]` != row.`[`len()`].
143    ///
144    /// [`column_count()`]: #method.column_count
145    /// [`len()`]: struct.Row.html#method.len
146    pub fn add_row(&mut self, row: Row) -> &mut Self {
147        let cells = row.0;
148
149        assert_eq!(
150            cells.len(),
151            self.n_columns,
152            "Number of columns in table and row don't match"
153        );
154
155        for (width, s) in self.column_widths.iter_mut().zip(cells.iter()) {
156            *width = ::std::cmp::max(*width, s.width());
157        }
158
159        self.rows.push(InternalRow::Cells(cells));
160        self
161    }
162
163    /// Convenience function for calling [`add_row`].
164    ///
165    /// # Panics
166    ///
167    /// The same as [`add_row`].
168    ///
169    /// [`add_row`]: #method.add_row
170    #[must_use]
171    pub fn with_row(mut self, row: Row) -> Self {
172        self.add_row(row);
173        self
174    }
175
176    /// Sets the string to output at the end of every line.
177    ///
178    /// By default this is `"\n"` on all platforms, like `println!`.
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// # use tabular::*;
184    /// #[cfg(windows)]
185    /// const DEFAULT_LINE_END: &'static str = "\r\n";
186    /// #[cfg(not(windows))]
187    /// const DEFAULT_LINE_END: &'static str = "\n";
188    ///
189    /// let table = Table::new("{:>} {:<}").set_line_end(DEFAULT_LINE_END)
190    ///     .with_row(Row::new().with_cell("x").with_cell("x"))
191    ///     .with_row(Row::new().with_cell("yy").with_cell("yy"))
192    ///     .with_row(Row::new().with_cell("zzz").with_cell("zzz"));
193    ///
194    /// assert_eq!( table.to_string(),
195    ///             format!("  x x{nl} yy yy{nl}zzz zzz{nl}", nl = DEFAULT_LINE_END) );
196    /// ```
197    ///
198    /// This works better than putting the carriage return in the format string:
199    ///
200    /// ```
201    /// # use tabular::*;
202    /// let table = Table::new("{:>} {:<}\r")
203    ///     .with_row(Row::new().with_cell("x").with_cell("x"))
204    ///     .with_row(Row::new().with_cell("yy").with_cell("yy"))
205    ///     .with_row(Row::new().with_cell("zzz").with_cell("zzz"));
206    ///
207    /// assert_eq!( table.to_string(),
208    ///             format!("  x x  \r\n yy yy \r\nzzz zzz\r\n") );
209    /// ```
210    ///
211    /// Note the trailing spaces. Trailing spaces mean that if any lines are wrapped
212    /// then all lines are wrapped.
213    #[must_use]
214    pub fn set_line_end<S: Into<String>>(mut self, line_end: S) -> Self {
215        self.line_end = line_end.into();
216        self
217    }
218}
219
220impl Debug for Table {
221    // This method allocates in two places:
222    //   - row_spec_to_string
223    //   - row.clone()
224    // It doesn't need to do either.
225    fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result {
226        write!(f, "Table::new({:?})", row_spec_to_string(&self.format))?;
227
228        if self.line_end != DEFAULT_LINE_END {
229            write!(f, ".set_line_end({:?})", self.line_end)?;
230        }
231
232        for row in &self.rows {
233            match *row {
234                InternalRow::Cells(ref row) => write!(f, ".with_row({:?})", Row(row.clone()))?,
235
236                InternalRow::Heading(ref heading) => write!(f, ".with_heading({:?})", heading)?,
237            }
238        }
239
240        Ok(())
241    }
242}
243
244impl Display for Table {
245    fn fmt(&self, f: &mut Formatter) -> ::std::fmt::Result {
246        use crate::column_spec::{Alignment::*, ColumnSpec::*};
247
248        let max_column_width = self.column_widths.iter().cloned().max().unwrap_or(0);
249        let mut spaces = String::with_capacity(max_column_width);
250        for _ in 0..max_column_width {
251            spaces.push(' ');
252        }
253
254        let mt_width_string = WidthString::default();
255        let is_not_last = |field_index| field_index + 1 < self.format.len();
256
257        for row in &self.rows {
258            match *row {
259                InternalRow::Cells(ref cells) => {
260                    let mut cw_iter = self.column_widths.iter().cloned();
261                    let mut row_iter = cells.iter();
262
263                    for field_index in 0..self.format.len() {
264                        match self.format[field_index] {
265                            Align(alignment) => {
266                                let cw = cw_iter.next().unwrap();
267                                let ws = row_iter.next().unwrap_or(&mt_width_string);
268                                let needed = cw - ws.width();
269                                let padding = &spaces[..needed];
270
271                                match alignment {
272                                    Left => {
273                                        f.write_str(ws.as_str())?;
274                                        if is_not_last(field_index) {
275                                            f.write_str(padding)?;
276                                        }
277                                    }
278
279                                    Center => {
280                                        let (before, after) = padding.split_at(needed / 2);
281                                        f.write_str(before)?;
282                                        f.write_str(ws.as_str())?;
283                                        if is_not_last(field_index) {
284                                            f.write_str(after)?;
285                                        }
286                                    }
287
288                                    Right => {
289                                        f.write_str(padding)?;
290                                        f.write_str(ws.as_str())?;
291                                    }
292                                }
293                            }
294
295                            Literal(ref s) => f.write_str(s)?,
296                        }
297                    }
298                }
299
300                InternalRow::Heading(ref s) => {
301                    f.write_str(s)?;
302                }
303            }
304            f.write_str(&self.line_end)?;
305        }
306
307        Ok(())
308    }
309}