1 | //! This module contains an [`ExtendedTable`] structure which is useful in cases where |
2 | //! a structure has a lot of fields. |
3 | //! |
4 | #![cfg_attr (feature = "derive" , doc = "```" )] |
5 | #![cfg_attr (not(feature = "derive" ), doc = "```ignore" )] |
6 | //! use tabled::{Tabled, tables::ExtendedTable}; |
7 | //! |
8 | //! #[derive(Tabled)] |
9 | //! struct Language { |
10 | //! name: &'static str, |
11 | //! designed_by: &'static str, |
12 | //! invented_year: usize, |
13 | //! } |
14 | //! |
15 | //! let languages = vec![ |
16 | //! Language{ |
17 | //! name: "C" , |
18 | //! designed_by: "Dennis Ritchie" , |
19 | //! invented_year: 1972 |
20 | //! }, |
21 | //! Language{ |
22 | //! name: "Rust" , |
23 | //! designed_by: "Graydon Hoare" , |
24 | //! invented_year: 2010 |
25 | //! }, |
26 | //! Language{ |
27 | //! name: "Go" , |
28 | //! designed_by: "Rob Pike" , |
29 | //! invented_year: 2009 |
30 | //! }, |
31 | //! ]; |
32 | //! |
33 | //! let table = ExtendedTable::new(languages).to_string(); |
34 | //! |
35 | //! let expected = "-[ RECORD 0 ]-+--------------- \n\ |
36 | //! name | C \n\ |
37 | //! designed_by | Dennis Ritchie \n\ |
38 | //! invented_year | 1972 \n\ |
39 | //! -[ RECORD 1 ]-+--------------- \n\ |
40 | //! name | Rust \n\ |
41 | //! designed_by | Graydon Hoare \n\ |
42 | //! invented_year | 2010 \n\ |
43 | //! -[ RECORD 2 ]-+--------------- \n\ |
44 | //! name | Go \n\ |
45 | //! designed_by | Rob Pike \n\ |
46 | //! invented_year | 2009" ; |
47 | //! |
48 | //! assert_eq!(table, expected); |
49 | //! ``` |
50 | |
51 | use std::borrow::Cow; |
52 | use std::fmt::{self, Display}; |
53 | |
54 | use crate::grid::util::string::string_width; |
55 | use crate::Tabled; |
56 | |
57 | /// `ExtendedTable` display data in a 'expanded display mode' from postgresql. |
58 | /// It may be useful for a large data sets with a lot of fields. |
59 | /// |
60 | /// See 'Examples' in <https://www.postgresql.org/docs/current/app-psql.html>. |
61 | /// |
62 | /// It escapes strings to resolve a multi-line ones. |
63 | /// Because of that ANSI sequences will be not be rendered too so colores will not be showed. |
64 | /// |
65 | /// ``` |
66 | /// use tabled::tables::ExtendedTable; |
67 | /// |
68 | /// let data = vec!["Hello" , "2021" ]; |
69 | /// let table = ExtendedTable::new(&data).to_string(); |
70 | /// |
71 | /// assert_eq!( |
72 | /// table, |
73 | /// concat!( |
74 | /// "-[ RECORD 0 ]- \n" , |
75 | /// "&str | Hello \n" , |
76 | /// "-[ RECORD 1 ]- \n" , |
77 | /// "&str | 2021" , |
78 | /// ) |
79 | /// ); |
80 | /// ``` |
81 | #[derive (Debug, Clone)] |
82 | pub struct ExtendedTable { |
83 | fields: Vec<String>, |
84 | records: Vec<Vec<String>>, |
85 | } |
86 | |
87 | impl ExtendedTable { |
88 | /// Creates a new instance of `ExtendedTable` |
89 | pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self |
90 | where |
91 | T: Tabled, |
92 | { |
93 | let data = iter |
94 | .into_iter() |
95 | .map(|i| { |
96 | i.fields() |
97 | .into_iter() |
98 | .map(|s| s.escape_debug().to_string()) |
99 | .collect() |
100 | }) |
101 | .collect(); |
102 | let header = T::headers() |
103 | .into_iter() |
104 | .map(|s| s.escape_debug().to_string()) |
105 | .collect(); |
106 | |
107 | Self { |
108 | records: data, |
109 | fields: header, |
110 | } |
111 | } |
112 | |
113 | /// Truncates table to a set width value for a table. |
114 | /// It returns a success inticator, where `false` means it's not possible to set the table width, |
115 | /// because of the given arguments. |
116 | /// |
117 | /// It tries to not affect fields, but if there's no enough space all records will be deleted and fields will be cut. |
118 | /// |
119 | /// The minimum width is 14. |
120 | pub fn truncate(&mut self, max: usize, suffix: &str) -> bool { |
121 | // -[ RECORD 0 ]- |
122 | let teplate_width = self.records.len().to_string().len() + 13; |
123 | let min_width = teplate_width; |
124 | if max < min_width { |
125 | return false; |
126 | } |
127 | |
128 | let suffix_width = string_width(suffix); |
129 | if max < suffix_width { |
130 | return false; |
131 | } |
132 | |
133 | let max = max - suffix_width; |
134 | |
135 | let fields_max_width = self |
136 | .fields |
137 | .iter() |
138 | .map(|s| string_width(s)) |
139 | .max() |
140 | .unwrap_or_default(); |
141 | |
142 | // 3 is a space for ' | ' |
143 | let fields_affected = max < fields_max_width + 3; |
144 | if fields_affected { |
145 | if max < 3 { |
146 | return false; |
147 | } |
148 | |
149 | let max = max - 3; |
150 | |
151 | if max < suffix_width { |
152 | return false; |
153 | } |
154 | |
155 | let max = max - suffix_width; |
156 | |
157 | truncate_fields(&mut self.fields, max, suffix); |
158 | truncate_records(&mut self.records, 0, suffix); |
159 | } else { |
160 | let max = max - fields_max_width - 3 - suffix_width; |
161 | truncate_records(&mut self.records, max, suffix); |
162 | } |
163 | |
164 | true |
165 | } |
166 | } |
167 | |
168 | impl From<Vec<Vec<String>>> for ExtendedTable { |
169 | fn from(mut data: Vec<Vec<String>>) -> Self { |
170 | if data.is_empty() { |
171 | return Self { |
172 | fields: vec![], |
173 | records: vec![], |
174 | }; |
175 | } |
176 | |
177 | let fields: Vec = data.remove(index:0); |
178 | |
179 | Self { |
180 | fields, |
181 | records: data, |
182 | } |
183 | } |
184 | } |
185 | |
186 | impl Display for ExtendedTable { |
187 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
188 | if self.records.is_empty() { |
189 | return Ok(()); |
190 | } |
191 | |
192 | // It's possible that field|header can be a multiline string so |
193 | // we escape it and trim \" chars. |
194 | let fields = self.fields.iter().collect::<Vec<_>>(); |
195 | |
196 | let max_field_width = fields |
197 | .iter() |
198 | .map(|s| string_width(s)) |
199 | .max() |
200 | .unwrap_or_default(); |
201 | |
202 | let max_values_length = self |
203 | .records |
204 | .iter() |
205 | .map(|record| record.iter().map(|s| string_width(s)).max()) |
206 | .max() |
207 | .unwrap_or_default() |
208 | .unwrap_or_default(); |
209 | |
210 | for (i, records) in self.records.iter().enumerate() { |
211 | write_header_template(f, i, max_field_width, max_values_length)?; |
212 | |
213 | for (value, field) in records.iter().zip(fields.iter()) { |
214 | writeln!(f)?; |
215 | write_record(f, field, value, max_field_width)?; |
216 | } |
217 | |
218 | let is_last_record = i + 1 == self.records.len(); |
219 | if !is_last_record { |
220 | writeln!(f)?; |
221 | } |
222 | } |
223 | |
224 | Ok(()) |
225 | } |
226 | } |
227 | |
228 | fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) { |
229 | for fields: &mut Vec in records { |
230 | truncate_fields(records:fields, max_width, suffix); |
231 | } |
232 | } |
233 | |
234 | fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) { |
235 | for text: &mut String in records { |
236 | truncate(text, max_width, suffix); |
237 | } |
238 | } |
239 | |
240 | fn write_header_template( |
241 | f: &mut fmt::Formatter<'_>, |
242 | index: usize, |
243 | max_field_width: usize, |
244 | max_values_length: usize, |
245 | ) -> fmt::Result { |
246 | let mut template = format!("-[ RECORD {index} ]-" ); |
247 | let default_template_length = template.len(); |
248 | |
249 | // 3 - is responsible for ' | ' formatting |
250 | let max_line_width = std::cmp::max( |
251 | max_field_width + 3 + max_values_length, |
252 | default_template_length, |
253 | ); |
254 | let rest_to_print = max_line_width - default_template_length; |
255 | if rest_to_print > 0 { |
256 | // + 1 is a space after field name and we get a next pos so its +2 |
257 | if max_field_width + 2 > default_template_length { |
258 | let part1 = (max_field_width + 1) - default_template_length; |
259 | let part2 = rest_to_print - part1 - 1; |
260 | |
261 | template.extend( |
262 | std::iter::repeat('-' ) |
263 | .take(part1) |
264 | .chain(std::iter::once('+' )) |
265 | .chain(std::iter::repeat('-' ).take(part2)), |
266 | ); |
267 | } else { |
268 | template.extend(std::iter::repeat('-' ).take(rest_to_print)); |
269 | } |
270 | } |
271 | |
272 | write!(f, " {template}" )?; |
273 | |
274 | Ok(()) |
275 | } |
276 | |
277 | fn write_record( |
278 | f: &mut fmt::Formatter<'_>, |
279 | field: &str, |
280 | value: &str, |
281 | max_field_width: usize, |
282 | ) -> fmt::Result { |
283 | write!(f, " {field:max_field_width$} | {value}" ) |
284 | } |
285 | |
286 | fn truncate(text: &mut String, max: usize, suffix: &str) { |
287 | let original_len: usize = text.len(); |
288 | |
289 | if max == 0 || text.is_empty() { |
290 | *text = String::new(); |
291 | } else { |
292 | *text = cut_str_basic(s:text, width:max).into_owned(); |
293 | } |
294 | |
295 | let cut_was_done: bool = text.len() < original_len; |
296 | if !suffix.is_empty() && cut_was_done { |
297 | text.push_str(string:suffix); |
298 | } |
299 | } |
300 | |
301 | fn cut_str_basic(s: &str, width: usize) -> Cow<'_, str> { |
302 | const REPLACEMENT: char = ' \u{FFFD}' ; |
303 | |
304 | let (length: usize, count_unknowns: usize, _) = split_at_pos(s, pos:width); |
305 | let buf: &str = &s[..length]; |
306 | if count_unknowns == 0 { |
307 | return Cow::Borrowed(buf); |
308 | } |
309 | |
310 | let mut buf: String = buf.to_owned(); |
311 | buf.extend(iter:std::iter::repeat(REPLACEMENT).take(count_unknowns)); |
312 | |
313 | Cow::Owned(buf) |
314 | } |
315 | |
316 | fn split_at_pos(s: &str, pos: usize) -> (usize, usize, usize) { |
317 | let mut length: usize = 0; |
318 | let mut i: usize = 0; |
319 | for c: char in s.chars() { |
320 | if i == pos { |
321 | break; |
322 | }; |
323 | |
324 | let c_width = unicode_width::UnicodeWidthChar::width(self:c).unwrap_or(0); |
325 | |
326 | // We cut the chars which takes more then 1 symbol to display, |
327 | // in order to archive the necessary width. |
328 | if i + c_width > pos { |
329 | let count: usize = pos - i; |
330 | return (length, count, c.len_utf8()); |
331 | } |
332 | |
333 | i += c_width; |
334 | length += c.len_utf8(); |
335 | } |
336 | |
337 | (length, 0, 0) |
338 | } |
339 | |