tabled/tables/
compact.rs

1//! This module contains a [`CompactTable`] table.
2
3use core::{cmp::max, fmt};
4
5use crate::{
6    grid::{
7        colors::NoColors,
8        config::{AlignmentHorizontal, CompactConfig, Indent, Sides},
9        dimension::{ConstDimension, ConstSize, Dimension},
10        records::{
11            into_records::{LimitColumns, LimitRows},
12            IntoRecords, IterRecords,
13        },
14        util::string::get_line_width,
15        CompactGrid,
16    },
17    settings::{style::Style, TableOption},
18};
19
20/// A table which consumes an [`IntoRecords`] iterator.
21/// It assumes that the content has only single line.
22///
23/// In contrast to [`Table`] [`CompactTable`] does no allocations but it consumes an iterator.
24/// It's useful when you don't want to re/allocate a buffer for your data.
25///
26/// # Example
27///
28/// It works smoothly with arrays.
29///
30#[cfg_attr(feature = "std", doc = "```")]
31#[cfg_attr(not(feature = "std"), doc = "```ignore")]
32/// use tabled::{settings::Style, tables::CompactTable};
33///
34/// let data = [
35///     ["FreeBSD", "1993", "William and Lynne Jolitz", "?"],
36///     ["OpenBSD", "1995", "Theo de Raadt", ""],
37///     ["HardenedBSD", "2014", "Oliver Pinter and Shawn Webb", ""],
38/// ];
39///
40/// let table = CompactTable::from(data)
41///     .with(Style::psql())
42///     .to_string();
43///
44/// assert_eq!(
45///     table,
46///     concat!(
47///         " FreeBSD     | 1993 | William and Lynne Jolitz     | ? \n",
48///         " OpenBSD     | 1995 | Theo de Raadt                |   \n",
49///         " HardenedBSD | 2014 | Oliver Pinter and Shawn Webb |   ",
50///     )
51/// );
52/// ```
53///
54/// But it's default creation requires to be given an estimated cell width, and the amount of columns.
55///
56#[cfg_attr(feature = "std", doc = "```")]
57#[cfg_attr(not(feature = "std"), doc = "```ignore")]
58/// use tabled::{settings::Style, tables::CompactTable};
59///
60/// let data = [
61///     ["FreeBSD", "1993", "William and Lynne Jolitz", "?"],
62///     ["OpenBSD", "1995", "Theo de Raadt", ""],
63///     ["HardenedBSD", "2014", "Oliver Pinter and Shawn Webb", ""],
64/// ];
65///
66/// // See what will happen if the given width is too narrow
67///
68/// let table = CompactTable::new(&data)
69///     .columns(4)
70///     .width(5)
71///     .with(Style::ascii())
72///     .to_string();
73///
74/// assert_eq!(
75///     table,
76///     "+-----+-----+-----+-----+\n\
77///      | FreeBSD | 1993 | William and Lynne Jolitz | ?   |\n\
78///      |-----+-----+-----+-----|\n\
79///      | OpenBSD | 1995 | Theo de Raadt |     |\n\
80///      |-----+-----+-----+-----|\n\
81///      | HardenedBSD | 2014 | Oliver Pinter and Shawn Webb |     |\n\
82///      +-----+-----+-----+-----+"
83/// );
84/// ```
85///
86/// [`Table`]: crate::Table
87#[derive(Debug, Clone)]
88pub struct CompactTable<I, D> {
89    records: I,
90    cfg: CompactConfig,
91    dims: D,
92    count_columns: usize,
93    count_rows: Option<usize>,
94}
95
96impl<I> CompactTable<I, ConstDimension<0, 0>> {
97    /// Creates a new [`CompactTable`] structure with a width dimension for all columns.
98    pub const fn new(iter: I) -> Self
99    where
100        I: IntoRecords,
101    {
102        Self {
103            records: iter,
104            cfg: create_config(),
105            count_columns: 0,
106            count_rows: None,
107            dims: ConstDimension::new(ConstSize::Value(2), ConstSize::Value(1)),
108        }
109    }
110}
111
112impl<I, const ROWS: usize, const COLS: usize> CompactTable<I, ConstDimension<COLS, ROWS>> {
113    /// Set a height for each row.
114    pub fn height<S: Into<ConstSize<COUNT_ROWS>>, const COUNT_ROWS: usize>(
115        self,
116        size: S,
117    ) -> CompactTable<I, ConstDimension<COLS, COUNT_ROWS>> {
118        let (width, _) = self.dims.into();
119        CompactTable {
120            dims: ConstDimension::new(width, size.into()),
121            records: self.records,
122            cfg: self.cfg,
123            count_columns: self.count_columns,
124            count_rows: self.count_rows,
125        }
126    }
127
128    /// Set a width for each column.
129    pub fn width<S: Into<ConstSize<COUNT_COLUMNS>>, const COUNT_COLUMNS: usize>(
130        self,
131        size: S,
132    ) -> CompactTable<I, ConstDimension<COUNT_COLUMNS, ROWS>> {
133        let (_, height) = self.dims.into();
134        CompactTable {
135            dims: ConstDimension::new(size.into(), height),
136            records: self.records,
137            cfg: self.cfg,
138            count_columns: self.count_columns,
139            count_rows: self.count_rows,
140        }
141    }
142}
143
144impl<I, D> CompactTable<I, D> {
145    /// Creates a new [`CompactTable`] structure with a known dimension.
146    ///
147    /// Notice that the function wont call [`Estimate`].
148    ///
149    /// [`Estimate`]: crate::grid::dimension::Estimate
150    pub fn with_dimension(iter: I, dimension: D) -> Self
151    where
152        I: IntoRecords,
153    {
154        Self {
155            records: iter,
156            dims: dimension,
157            cfg: create_config(),
158            count_columns: 0,
159            count_rows: None,
160        }
161    }
162
163    /// With is a generic function which applies options to the [`CompactTable`].
164    pub fn with<O>(mut self, option: O) -> Self
165    where
166        for<'a> O: TableOption<IterRecords<&'a I>, CompactConfig, D>,
167    {
168        let mut records = IterRecords::new(&self.records, self.count_columns, self.count_rows);
169        option.change(&mut records, &mut self.cfg, &mut self.dims);
170
171        self
172    }
173
174    /// Limit a number of rows.
175    pub const fn rows(mut self, count_rows: usize) -> Self {
176        self.count_rows = Some(count_rows);
177        self
178    }
179
180    /// Limit a number of columns.
181    pub const fn columns(mut self, count: usize) -> Self {
182        self.count_columns = count;
183        self
184    }
185
186    /// Returns a table config.
187    pub fn get_config(&self) -> &CompactConfig {
188        &self.cfg
189    }
190
191    /// Returns a table config.
192    pub fn get_config_mut(&mut self) -> &mut CompactConfig {
193        &mut self.cfg
194    }
195
196    /// Format table into [fmt::Write]er.
197    pub fn fmt<W>(self, writer: W) -> fmt::Result
198    where
199        I: IntoRecords,
200        I::Cell: AsRef<str>,
201        D: Dimension,
202        W: fmt::Write,
203    {
204        build_grid(
205            writer,
206            self.records,
207            self.dims,
208            self.cfg,
209            self.count_columns,
210            self.count_rows,
211        )
212    }
213
214    /// Format table into a writer.
215    #[cfg(feature = "std")]
216    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
217    pub fn build<W>(self, writer: W) -> std::io::Result<()>
218    where
219        I: IntoRecords,
220        I::Cell: AsRef<str>,
221        D: Dimension,
222        W: std::io::Write,
223    {
224        let writer = crate::util::utf8_writer::UTF8Writer::new(writer);
225        self.fmt(writer).map_err(std::io::Error::other)
226    }
227
228    /// Build a string.
229    ///
230    /// We can't implement [`std::string::ToString`] cause it does takes `&self` reference.
231    #[allow(clippy::inherent_to_string)]
232    #[cfg(feature = "std")]
233    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
234    pub fn to_string(self) -> String
235    where
236        I: IntoRecords,
237        I::Cell: AsRef<str>,
238        D: Dimension,
239    {
240        let mut buf = String::new();
241        self.fmt(&mut buf)
242            .expect("it's expected to be ok according to doc");
243
244        buf
245    }
246}
247
248impl<T, const ROWS: usize, const COLS: usize> From<[[T; COLS]; ROWS]>
249    for CompactTable<[[T; COLS]; ROWS], ConstDimension<COLS, ROWS>>
250where
251    T: AsRef<str>,
252{
253    fn from(mat: [[T; COLS]; ROWS]) -> Self {
254        let mut width = [0; COLS];
255        for row in mat.iter() {
256            for (col, text) in row.iter().enumerate() {
257                let text = text.as_ref();
258                let text_width = get_line_width(text);
259                width[col] = max(width[col], text_width);
260            }
261        }
262
263        // add padding
264        for w in &mut width {
265            *w += 2;
266        }
267
268        let dims = ConstDimension::new(ConstSize::List(width), ConstSize::Value(1));
269        Self::with_dimension(mat, dims).columns(COLS).rows(ROWS)
270    }
271}
272
273fn build_grid<W, I, D>(
274    writer: W,
275    records: I,
276    dims: D,
277    config: CompactConfig,
278    cols: usize,
279    rows: Option<usize>,
280) -> fmt::Result
281where
282    W: fmt::Write,
283    I: IntoRecords,
284    I::Cell: AsRef<str>,
285    D: Dimension,
286{
287    match rows {
288        Some(limit) => {
289            let records = LimitRows::new(records, limit);
290            let records = LimitColumns::new(records, cols);
291            let records = IterRecords::new(records, cols, rows);
292            CompactGrid::new(records, config, dims, NoColors).build(writer)
293        }
294        None => {
295            let records = LimitColumns::new(records, cols);
296            let records = IterRecords::new(records, cols, rows);
297            CompactGrid::new(records, config, dims, NoColors).build(writer)
298        }
299    }
300}
301
302const fn create_config() -> CompactConfig {
303    CompactConfig::new()
304        .set_padding(Sides::new(
305            Indent::spaced(1),
306            Indent::spaced(1),
307            Indent::zero(),
308            Indent::zero(),
309        ))
310        .set_alignment_horizontal(AlignmentHorizontal::Left)
311        .set_borders(Style::ascii().get_borders())
312}
313
314impl<R, D> TableOption<R, CompactConfig, D> for CompactConfig {
315    fn change(self, _: &mut R, cfg: &mut CompactConfig, _: &mut D) {
316        *cfg = self;
317    }
318}