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::fmt::{self, Display};
52
53use crate::grid::util::string::string_width;
54use crate::Tabled;
55
56/// `ExtendedTable` display data in a 'expanded display mode' from postgresql.
57/// It may be useful for a large data sets with a lot of fields.
58///
59/// See 'Examples' in <https://www.postgresql.org/docs/current/app-psql.html>.
60///
61/// It escapes strings to resolve a multi-line ones.
62/// Because of that ANSI sequences will be not be rendered too so colores will not be showed.
63///
64/// ```
65/// use tabled::tables::ExtendedTable;
66///
67/// let data = vec!["Hello", "2021"];
68/// let table = ExtendedTable::new(&data).to_string();
69///
70/// assert_eq!(
71/// table,
72/// concat!(
73/// "-[ RECORD 0 ]-\n",
74/// "&str | Hello\n",
75/// "-[ RECORD 1 ]-\n",
76/// "&str | 2021",
77/// )
78/// );
79/// ```
80#[derive(Debug, Clone)]
81pub struct ExtendedTable {
82 fields: Vec<String>,
83 records: Vec<Vec<String>>,
84}
85
86impl ExtendedTable {
87 /// Creates a new instance of `ExtendedTable`
88 pub fn new<T>(iter: impl IntoIterator<Item = T>) -> Self
89 where
90 T: Tabled,
91 {
92 let data = iter
93 .into_iter()
94 .map(|i| {
95 i.fields()
96 .into_iter()
97 .map(|s| s.escape_debug().to_string())
98 .collect()
99 })
100 .collect();
101 let header = T::headers()
102 .into_iter()
103 .map(|s| s.escape_debug().to_string())
104 .collect();
105
106 Self {
107 records: data,
108 fields: header,
109 }
110 }
111
112 /// Truncates table to a set width value for a table.
113 /// It returns a success inticator, where `false` means it's not possible to set the table width,
114 /// because of the given arguments.
115 ///
116 /// It tries to not affect fields, but if there's no enough space all records will be deleted and fields will be cut.
117 ///
118 /// The minimum width is 14.
119 pub fn truncate(&mut self, max: usize, suffix: &str) -> bool {
120 // -[ RECORD 0 ]-
121 let teplate_width = self.records.len().to_string().len() + 13;
122 let min_width = teplate_width;
123 if max < min_width {
124 return false;
125 }
126
127 let suffix_width = string_width(suffix);
128 if max < suffix_width {
129 return false;
130 }
131
132 let max = max - suffix_width;
133
134 let fields_max_width = self
135 .fields
136 .iter()
137 .map(|s| string_width(s))
138 .max()
139 .unwrap_or_default();
140
141 // 3 is a space for ' | '
142 let fields_affected = max < fields_max_width + 3;
143 if fields_affected {
144 if max < 3 {
145 return false;
146 }
147
148 let max = max - 3;
149
150 if max < suffix_width {
151 return false;
152 }
153
154 let max = max - suffix_width;
155
156 truncate_fields(&mut self.fields, max, suffix);
157 truncate_records(&mut self.records, 0, suffix);
158 } else {
159 let max = max - fields_max_width - 3 - suffix_width;
160 truncate_records(&mut self.records, max, suffix);
161 }
162
163 true
164 }
165}
166
167impl From<Vec<Vec<String>>> for ExtendedTable {
168 fn from(mut data: Vec<Vec<String>>) -> Self {
169 if data.is_empty() {
170 return Self {
171 fields: vec![],
172 records: vec![],
173 };
174 }
175
176 let fields: Vec = data.remove(index:0);
177
178 Self {
179 fields,
180 records: data,
181 }
182 }
183}
184
185impl Display for ExtendedTable {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 if self.records.is_empty() {
188 return Ok(());
189 }
190
191 // It's possible that field|header can be a multiline string so
192 // we escape it and trim \" chars.
193 let fields = self.fields.iter().collect::<Vec<_>>();
194
195 let max_field_width = fields
196 .iter()
197 .map(|s| string_width(s))
198 .max()
199 .unwrap_or_default();
200
201 let max_values_length = self
202 .records
203 .iter()
204 .map(|record| record.iter().map(|s| string_width(s)).max())
205 .max()
206 .unwrap_or_default()
207 .unwrap_or_default();
208
209 for (i, records) in self.records.iter().enumerate() {
210 write_header_template(f, i, max_field_width, max_values_length)?;
211
212 for (value, field) in records.iter().zip(fields.iter()) {
213 writeln!(f)?;
214 write_record(f, field, value, max_field_width)?;
215 }
216
217 let is_last_record = i + 1 == self.records.len();
218 if !is_last_record {
219 writeln!(f)?;
220 }
221 }
222
223 Ok(())
224 }
225}
226
227fn truncate_records(records: &mut Vec<Vec<String>>, max_width: usize, suffix: &str) {
228 for fields: &mut Vec in records {
229 truncate_fields(records:fields, max_width, suffix);
230 }
231}
232
233fn truncate_fields(records: &mut Vec<String>, max_width: usize, suffix: &str) {
234 for text: &mut String in records {
235 truncate(text, max_width, suffix);
236 }
237}
238
239fn write_header_template(
240 f: &mut fmt::Formatter<'_>,
241 index: usize,
242 max_field_width: usize,
243 max_values_length: usize,
244) -> fmt::Result {
245 let mut template = format!("-[ RECORD {index} ]-");
246 let default_template_length = template.len();
247
248 // 3 - is responsible for ' | ' formatting
249 let max_line_width = std::cmp::max(
250 max_field_width + 3 + max_values_length,
251 default_template_length,
252 );
253 let rest_to_print = max_line_width - default_template_length;
254 if rest_to_print > 0 {
255 // + 1 is a space after field name and we get a next pos so its +2
256 if max_field_width + 2 > default_template_length {
257 let part1 = (max_field_width + 1) - default_template_length;
258 let part2 = rest_to_print - part1 - 1;
259
260 template.extend(
261 std::iter::repeat('-')
262 .take(part1)
263 .chain(std::iter::once('+'))
264 .chain(std::iter::repeat('-').take(part2)),
265 );
266 } else {
267 template.extend(std::iter::repeat('-').take(rest_to_print));
268 }
269 }
270
271 write!(f, "{template}")?;
272
273 Ok(())
274}
275
276fn write_record(
277 f: &mut fmt::Formatter<'_>,
278 field: &str,
279 value: &str,
280 max_field_width: usize,
281) -> fmt::Result {
282 write!(f, "{field:max_field_width$} | {value}")
283}
284
285fn truncate(text: &mut String, max: usize, suffix: &str) {
286 let original_len: usize = text.len();
287
288 if max == 0 || text.is_empty() {
289 *text = String::new();
290 } else {
291 *text = crate::util::string::cut_str2(text, width:max).into_owned();
292 }
293
294 let cut_was_done: bool = text.len() < original_len;
295 if !suffix.is_empty() && cut_was_done {
296 text.push_str(string:suffix);
297 }
298}
299