1//! This module contains a [`IterTable`] table.
2//!
3//! In contrast to [`Table`] [`IterTable`] does no allocations but it consumes an iterator.
4//! It's useful when you don't want to re/allocate a buffer for your data.
5//!
6//! # Example
7//!
8//! ```
9//! use tabled::{grid::records::IterRecords, tables::IterTable};
10//!
11//! let iterator = vec![vec!["First", "row"], vec!["Second", "row"]];
12//! let records = IterRecords::new(iterator, 2, Some(2));
13//! let table = IterTable::new(records);
14//!
15//! let s = table.to_string();
16//!
17//! assert_eq!(
18//! s,
19//! "+--------+-----+\n\
20//! | First | row |\n\
21//! +--------+-----+\n\
22//! | Second | row |\n\
23//! +--------+-----+",
24//! );
25//! ```
26//!
27//! [`Table`]: crate::Table
28
29use std::{fmt, io};
30
31use crate::{
32 grid::{
33 colors::NoColors,
34 config::{AlignmentHorizontal, CompactConfig, Indent, Sides, SpannedConfig},
35 dimension::{CompactGridDimension, Dimension, DimensionValue, StaticDimension},
36 records::{
37 into_records::{BufRecords, LimitColumns, LimitRows, TruncateContent},
38 IntoRecords, IterRecords,
39 },
40 Grid,
41 },
42 settings::{Style, TableOption},
43};
44
45use super::util::utf8_writer::UTF8Writer;
46
47/// A table which consumes an [`IntoRecords`] iterator.
48///
49/// To be able to build table we need a dimensions.
50/// If no width and count_columns is set, [`IterTable`] will sniff the records, by
51/// keeping a number of rows buffered (You can set the number via [`IterTable::sniff`]).
52#[derive(Debug, Clone)]
53pub struct IterTable<I> {
54 records: I,
55 cfg: CompactConfig,
56 table: Settings,
57}
58
59#[derive(Debug, Clone)]
60struct Settings {
61 sniff: usize,
62 count_columns: Option<usize>,
63 count_rows: Option<usize>,
64 width: Option<usize>,
65 height: Option<usize>,
66}
67
68impl<I> IterTable<I> {
69 /// Creates a new [`IterTable`] structure.
70 pub fn new(iter: I) -> Self
71 where
72 I: IntoRecords,
73 {
74 Self {
75 records: iter,
76 cfg: create_config(),
77 table: Settings {
78 sniff: 1000,
79 count_columns: None,
80 count_rows: None,
81 height: None,
82 width: None,
83 },
84 }
85 }
86
87 /// With is a generic function which applies options to the [`IterTable`].
88 pub fn with<O>(mut self, option: O) -> Self
89 where
90 for<'a> O: TableOption<IterRecords<&'a I>, CompactConfig, StaticDimension>,
91 {
92 let count_columns = self.table.count_columns.unwrap_or(0);
93 let mut records = IterRecords::new(&self.records, count_columns, self.table.count_rows);
94 let mut dims = StaticDimension::new(DimensionValue::Exact(0), DimensionValue::Exact(1));
95 option.change(&mut records, &mut self.cfg, &mut dims);
96
97 self
98 }
99
100 /// Limit a number of columns.
101 pub fn columns(mut self, count_columns: usize) -> Self {
102 self.table.count_columns = Some(count_columns);
103 self
104 }
105
106 /// Limit a number of rows.
107 pub fn rows(mut self, count_rows: usize) -> Self {
108 self.table.count_rows = Some(count_rows);
109 self
110 }
111
112 /// Limit an amount of rows will be read for dimension estimations.
113 pub fn sniff(mut self, count: usize) -> Self {
114 self.table.sniff = count;
115 self
116 }
117
118 /// Set a height for each row.
119 pub fn height(mut self, size: usize) -> Self {
120 self.table.height = Some(size);
121 self
122 }
123
124 /// Set a width for each column.
125 pub fn width(mut self, size: usize) -> Self {
126 self.table.width = Some(size);
127 self
128 }
129
130 /// Build a string.
131 ///
132 /// We can't implement [`std::string::ToString`] cause it does takes `&self` reference.
133 #[allow(clippy::inherent_to_string)]
134 pub fn to_string(self) -> String
135 where
136 I: IntoRecords,
137 I::Cell: AsRef<str>,
138 {
139 let mut buf = String::new();
140 self.fmt(&mut buf)
141 .expect("according to a doc is safe to fmt() a string");
142
143 buf
144 }
145
146 /// Format table into [`io::Write`]r.
147 pub fn build<W>(self, writer: W) -> io::Result<()>
148 where
149 W: io::Write,
150 I: IntoRecords,
151 I::Cell: AsRef<str>,
152 {
153 let writer = UTF8Writer::new(writer);
154 self.fmt(writer)
155 .map_err(|err| io::Error::new(io::ErrorKind::Other, err))
156 }
157
158 /// Format table into [fmt::Write]er.
159 pub fn fmt<W>(self, writer: W) -> fmt::Result
160 where
161 W: fmt::Write,
162 I: IntoRecords,
163 I::Cell: AsRef<str>,
164 {
165 build_grid(writer, self.records, self.cfg, self.table)
166 }
167}
168
169fn build_grid<W, I>(f: W, iter: I, cfg: CompactConfig, opts: Settings) -> fmt::Result
170where
171 W: fmt::Write,
172 I: IntoRecords,
173 I::Cell: AsRef<str>,
174{
175 let width_config: bool = opts.width.is_some() && opts.count_columns.is_some();
176 if width_config {
177 build_table_with_static_dims(f, iter, cfg, opts)
178 } else if opts.width.is_some() {
179 build_table_sniffing_with_width(f, iter, cfg, opts)
180 } else {
181 build_table_sniffing(f, iter, cfg, opts)
182 }
183}
184
185fn build_table_with_static_dims<W, I>(
186 f: W,
187 iter: I,
188 cfg: CompactConfig,
189 opts: Settings,
190) -> fmt::Result
191where
192 W: fmt::Write,
193 I: IntoRecords,
194 I::Cell: AsRef<str>,
195{
196 let count_columns: usize = opts.count_columns.unwrap();
197 let width: usize = opts.width.unwrap();
198 let height: usize = opts.height.unwrap_or(default:1);
199 let contentw: WidthDimension = WidthDimension::Exact(width);
200 let pad: &Sides = cfg.get_padding();
201 let w: DimensionValue = DimensionValue::Exact(width + pad.left.size + pad.right.size);
202 let h: DimensionValue = DimensionValue::Exact(height + pad.top.size + pad.bottom.size);
203 let dims: StaticDimension = StaticDimension::new(width:w, height:h);
204 let cfg: SpannedConfig = SpannedConfig::from(cfg);
205
206 match opts.count_rows {
207 Some(limit: usize) => {
208 let records: LimitRows = LimitRows::new(records:iter, limit);
209 let records: IterRecords> = build_records(records, width:contentw, count_columns, count_rows:Some(limit));
210 Grid::new(records, dimension:dims, config:cfg, colors:NoColors).build(f)
211 }
212 None => {
213 let records: IterRecords> = build_records(records:iter, width:contentw, count_columns, count_rows:None);
214 Grid::new(records, dimension:dims, config:cfg, colors:NoColors).build(f)
215 }
216 }
217}
218
219fn build_table_sniffing<W, I>(f: W, iter: I, cfg: CompactConfig, opts: Settings) -> fmt::Result
220where
221 W: fmt::Write,
222 I: IntoRecords,
223 I::Cell: AsRef<str>,
224{
225 let records = BufRecords::new(iter, opts.sniff);
226
227 let count_columns = get_count_columns(&opts, records.as_slice());
228
229 let (mut width, height) = {
230 let records = LimitColumns::new(records.as_slice(), count_columns);
231 let records = IterRecords::new(records, count_columns, None);
232 CompactGridDimension::dimension(records, &cfg)
233 };
234
235 let padding = cfg.get_padding();
236 let pad = padding.left.size + padding.right.size;
237 let padv = padding.top.size + padding.bottom.size;
238
239 if opts.sniff == 0 {
240 width = std::iter::repeat(pad)
241 .take(count_columns)
242 .collect::<Vec<_>>();
243 }
244
245 let content_width = WidthDimension::List(width.iter().map(|i| i.saturating_sub(pad)).collect());
246 let dims_width = DimensionValue::List(width);
247
248 let height_exact = opts.height.unwrap_or(1) + padv;
249 let mut dims_height = DimensionValue::Partial(height, height_exact);
250
251 if opts.height.is_some() {
252 dims_height = DimensionValue::Exact(height_exact);
253 }
254
255 let dims = StaticDimension::new(dims_width, dims_height);
256 let cfg = SpannedConfig::from(cfg);
257
258 match opts.count_rows {
259 Some(limit) => {
260 let records = LimitRows::new(records, limit);
261 let records = build_records(records, content_width, count_columns, Some(limit));
262 Grid::new(records, dims, cfg, NoColors).build(f)
263 }
264 None => {
265 let records = build_records(records, content_width, count_columns, None);
266 Grid::new(records, dims, cfg, NoColors).build(f)
267 }
268 }
269}
270
271fn build_table_sniffing_with_width<W, I>(
272 f: W,
273 iter: I,
274 cfg: CompactConfig,
275 opts: Settings,
276) -> fmt::Result
277where
278 W: fmt::Write,
279 I: IntoRecords,
280 I::Cell: AsRef<str>,
281{
282 let records = BufRecords::new(iter, opts.sniff);
283
284 let count_columns = get_count_columns(&opts, records.as_slice());
285
286 let width = opts.width.unwrap();
287 let contentw = WidthDimension::Exact(width);
288
289 let padding = cfg.get_padding();
290 let pad = padding.left.size + padding.right.size;
291 let padv = padding.top.size + padding.bottom.size;
292
293 let height = opts.height.unwrap_or(1) + padv;
294 let dimsh = DimensionValue::Exact(height);
295 let dimsw = DimensionValue::Exact(width + pad);
296 let dims = StaticDimension::new(dimsw, dimsh);
297
298 let cfg = SpannedConfig::from(cfg);
299
300 match opts.count_rows {
301 Some(limit) => {
302 let records = LimitRows::new(records, limit);
303 let records = build_records(records, contentw, count_columns, Some(limit));
304 Grid::new(records, dims, cfg, NoColors).build(f)
305 }
306 None => {
307 let records = build_records(records, contentw, count_columns, None);
308 Grid::new(records, dims, cfg, NoColors).build(f)
309 }
310 }
311}
312
313fn get_count_columns<T>(opts: &Settings, buf: &[Vec<T>]) -> usize {
314 match opts.count_columns {
315 Some(size: usize) => size,
316 None => buf.iter().map(|row| row.len()).max().unwrap_or(default:0),
317 }
318}
319
320fn create_config() -> CompactConfig {
321 CompactConfigCompactConfig::default()
322 .set_padding(Sides::new(
323 Indent::spaced(1),
324 Indent::spaced(1),
325 Indent::default(),
326 Indent::default(),
327 ))
328 .set_alignment_horizontal(alignment:AlignmentHorizontal::Left)
329 .set_borders(Style::ascii().get_borders())
330}
331
332fn build_records<I>(
333 records: I,
334 width: WidthDimension,
335 count_columns: usize,
336 count_rows: Option<usize>,
337) -> IterRecords<LimitColumns<TruncateContent<I, WidthDimension>>>
338where
339 I: IntoRecords,
340{
341 let records: TruncateContent = TruncateContent::new(records, dimension:width);
342 let records: LimitColumns> = LimitColumns::new(records, limit:count_columns);
343 IterRecords::new(iter:records, count_columns, count_rows)
344}
345
346/// A dimension value.
347#[derive(Debug, Clone)]
348enum WidthDimension {
349 Exact(usize),
350 List(Vec<usize>),
351}
352
353impl Dimension for WidthDimension {
354 fn get_width(&self, column: usize) -> usize {
355 match self {
356 WidthDimension::Exact(value: &usize) => *value,
357 WidthDimension::List(list: &Vec) => list[column],
358 }
359 }
360
361 fn get_height(&self, _row: usize) -> usize {
362 unreachable!("A height method is not supposed to be called");
363 }
364}
365