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, DimensionValue, StaticDimension},
36 records::{
37 into_records::{
38 truncate_records::ExactValue, BufColumns, BufRows, LimitColumns, LimitRows,
39 TruncateContent,
40 },
41 IntoRecords, IterRecords,
42 },
43 Grid,
44 },
45 settings::{Style, TableOption},
46};
47
48use super::util::utf8_writer::UTF8Writer;
49
50/// A table which consumes an [`IntoRecords`] iterator.
51///
52/// To be able to build table we need a dimensions.
53/// If no width and count_columns is set, [`IterTable`] will sniff the records, by
54/// keeping a number of rows buffered (You can set the number via [`IterTable::sniff`]).
55#[derive(Debug, Clone)]
56pub struct IterTable<I> {
57 records: I,
58 cfg: CompactConfig,
59 table: Settings,
60}
61
62#[derive(Debug, Clone)]
63struct Settings {
64 sniff: usize,
65 count_columns: Option<usize>,
66 count_rows: Option<usize>,
67 width: Option<usize>,
68 height: Option<usize>,
69}
70
71impl<I> IterTable<I> {
72 /// Creates a new [`IterTable`] structure.
73 pub fn new(iter: I) -> Self
74 where
75 I: IntoRecords,
76 {
77 Self {
78 records: iter,
79 cfg: create_config(),
80 table: Settings {
81 sniff: 1000,
82 count_columns: None,
83 count_rows: None,
84 height: None,
85 width: None,
86 },
87 }
88 }
89
90 /// With is a generic function which applies options to the [`IterTable`].
91 pub fn with<O>(mut self, option: O) -> Self
92 where
93 for<'a> O: TableOption<IterRecords<&'a I>, StaticDimension, CompactConfig>,
94 {
95 let count_columns = self.table.count_columns.unwrap_or(0);
96 let mut records = IterRecords::new(&self.records, count_columns, self.table.count_rows);
97 let mut dims = StaticDimension::new(DimensionValue::Exact(0), DimensionValue::Exact(1));
98 option.change(&mut records, &mut self.cfg, &mut dims);
99
100 self
101 }
102
103 /// Limit a number of columns.
104 pub fn columns(mut self, count_columns: usize) -> Self {
105 self.table.count_columns = Some(count_columns);
106 self
107 }
108
109 /// Limit a number of rows.
110 pub fn rows(mut self, count_rows: usize) -> Self {
111 self.table.count_rows = Some(count_rows);
112 self
113 }
114
115 /// Limit an amount of rows will be read for dimension estimations.
116 pub fn sniff(mut self, count: usize) -> Self {
117 self.table.sniff = count;
118 self
119 }
120
121 /// Set a height for each row.
122 pub fn height(mut self, size: usize) -> Self {
123 self.table.height = Some(size);
124 self
125 }
126
127 /// Set a width for each column.
128 pub fn width(mut self, size: usize) -> Self {
129 self.table.width = Some(size);
130 self
131 }
132
133 /// Build a string.
134 ///
135 /// We can't implement [`std::string::ToString`] cause it does takes `&self` reference.
136 #[allow(clippy::inherent_to_string)]
137 pub fn to_string(self) -> String
138 where
139 I: IntoRecords,
140 {
141 let mut buf = String::new();
142 self.fmt(&mut buf).expect("safe");
143
144 buf
145 }
146
147 /// Format table into [`io::Write`]r.
148 pub fn build<W>(self, writer: W) -> io::Result<()>
149 where
150 I: IntoRecords,
151 W: io::Write,
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 I: IntoRecords,
162 W: fmt::Write,
163 {
164 build_grid(writer, self.records, self.cfg, self.table)
165 }
166}
167
168fn build_grid<W: fmt::Write, I: IntoRecords>(
169 f: W,
170 iter: I,
171 cfg: CompactConfig,
172 opts: Settings,
173) -> fmt::Result {
174 let dont_sniff: bool = opts.width.is_some() && opts.count_columns.is_some();
175 if dont_sniff {
176 build_table_with_static_dims(f, iter, cfg, opts)
177 } else if opts.width.is_none() {
178 build_table_sniffing_with_unknown_width(f, iter, cfg, opts)
179 } else {
180 build_table_sniffing_with_known_width(f, iter, cfg, opts)
181 }
182}
183
184fn build_table_with_static_dims<W, I>(
185 f: W,
186 iter: I,
187 cfg: CompactConfig,
188 opts: Settings,
189) -> fmt::Result
190where
191 W: fmt::Write,
192 I: IntoRecords,
193{
194 let count_columns: usize = opts.count_columns.unwrap();
195 let width: usize = opts.width.unwrap();
196 let height: usize = opts.height.unwrap_or(default:1);
197 let contentw: ExactValue<'_> = ExactValue::Exact(width);
198 let pad: &Sides = cfg.get_padding();
199 let w: DimensionValue = DimensionValue::Exact(width + pad.left.size + pad.right.size);
200 let h: DimensionValue = DimensionValue::Exact(height + pad.top.size + pad.bottom.size);
201 let dims: StaticDimension = StaticDimension::new(width:w, height:h);
202 let cfg: SpannedConfig = SpannedConfig::from(cfg);
203
204 match opts.count_rows {
205 Some(limit: usize) => {
206 let records: LimitRows = LimitRows::new(records:iter, limit);
207 let records: IterRecords> = build_records(records, width:contentw, count_columns, count_rows:Some(limit));
208 Grid::new(records, dimension:dims, config:cfg, colors:NoColors).build(f)
209 }
210 None => {
211 let records: IterRecords> = build_records(records:iter, width:contentw, count_columns, count_rows:None);
212 Grid::new(records, dimension:dims, config:cfg, colors:NoColors).build(f)
213 }
214 }
215}
216
217fn build_table_sniffing_with_unknown_width<W, I>(
218 f: W,
219 iter: I,
220 cfg: CompactConfig,
221 opts: Settings,
222) -> fmt::Result
223where
224 W: fmt::Write,
225 I: IntoRecords,
226{
227 let records = BufRows::new(iter, opts.sniff);
228 let records = BufColumns::from(records);
229
230 let count_columns = get_count_columns(&opts, records.as_slice());
231
232 let (mut width, height) = {
233 let records = LimitColumns::new(records.as_slice(), count_columns);
234 let records = IterRecords::new(records, count_columns, None);
235 CompactGridDimension::dimension(records, &cfg)
236 };
237
238 let padding = cfg.get_padding();
239 let pad = padding.left.size + padding.right.size;
240 let padv = padding.top.size + padding.bottom.size;
241
242 if opts.sniff == 0 {
243 width = std::iter::repeat(pad)
244 .take(count_columns)
245 .collect::<Vec<_>>();
246 }
247
248 let content_width = ExactValue::List(width.iter().map(|i| i.saturating_sub(pad)).collect());
249 let dims_width = DimensionValue::List(width);
250
251 let height_exact = opts.height.unwrap_or(1) + padv;
252 let mut dims_height = DimensionValue::Partial(height, height_exact);
253
254 if opts.height.is_some() {
255 dims_height = DimensionValue::Exact(height_exact);
256 }
257
258 let dims = StaticDimension::new(dims_width, dims_height);
259 let cfg = SpannedConfig::from(cfg);
260
261 match opts.count_rows {
262 Some(limit) => {
263 let records = LimitRows::new(records, limit);
264 let records = build_records(records, content_width, count_columns, Some(limit));
265 Grid::new(records, dims, cfg, NoColors).build(f)
266 }
267 None => {
268 let records = build_records(records, content_width, count_columns, None);
269 Grid::new(records, dims, cfg, NoColors).build(f)
270 }
271 }
272}
273
274fn build_table_sniffing_with_known_width<W, I>(
275 f: W,
276 iter: I,
277 cfg: CompactConfig,
278 opts: Settings,
279) -> fmt::Result
280where
281 W: fmt::Write,
282 I: IntoRecords,
283{
284 let records = BufRows::new(iter, opts.sniff);
285 let records = BufColumns::from(records);
286
287 let count_columns = get_count_columns(&opts, records.as_slice());
288
289 let width = opts.width.unwrap();
290 let contentw = ExactValue::Exact(width);
291
292 let padding = cfg.get_padding();
293 let pad = padding.left.size + padding.right.size;
294 let padv = padding.top.size + padding.bottom.size;
295
296 let height = opts.height.unwrap_or(1) + padv;
297 let dimsh = DimensionValue::Exact(height);
298 let dimsw = DimensionValue::Exact(width + pad);
299 let dims = StaticDimension::new(dimsw, dimsh);
300
301 let cfg = SpannedConfig::from(cfg);
302
303 match opts.count_rows {
304 Some(limit) => {
305 let records = LimitRows::new(records, limit);
306 let records = build_records(records, contentw, count_columns, Some(limit));
307 Grid::new(records, dims, cfg, NoColors).build(f)
308 }
309 None => {
310 let records = build_records(records, contentw, count_columns, None);
311 Grid::new(records, dims, cfg, NoColors).build(f)
312 }
313 }
314}
315
316fn get_count_columns(opts: &Settings, buf: &[Vec<String>]) -> usize {
317 match opts.count_columns {
318 Some(size: usize) => size,
319 None => buf.iter().map(|row| row.len()).max().unwrap_or(default:0),
320 }
321}
322
323fn create_config() -> CompactConfig {
324 CompactConfigCompactConfig::default()
325 .set_padding(Sides::new(
326 Indent::spaced(1),
327 Indent::spaced(1),
328 Indent::default(),
329 Indent::default(),
330 ))
331 .set_alignment_horizontal(alignment:AlignmentHorizontal::Left)
332 .set_borders(*Style::ascii().get_borders())
333}
334
335fn build_records<I: IntoRecords>(
336 records: I,
337 width: ExactValue<'_>,
338 count_columns: usize,
339 count_rows: Option<usize>,
340) -> IterRecords<LimitColumns<TruncateContent<'_, I>>> {
341 let records: TruncateContent<'_, I> = TruncateContent::new(records, width);
342 let records: LimitColumns> = LimitColumns::new(records, limit:count_columns);
343 IterRecords::new(iter:records, count_columns, count_rows)
344}
345