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}