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
51use std::borrow::Cow;
52use std::fmt::{self, Display};
53
54use crate::grid::util::string::string_width;
55use 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)]
82pub struct ExtendedTable {
83 fields: Vec<String>,
84 records: Vec<Vec<String>>,
85}
86
87impl 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
168impl 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
186impl 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
228fn 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
234fn 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
240fn 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
277fn 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
286fn 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
301fn 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
316fn 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